]> git.lizzy.rs Git - rust.git/blob - editors/emacs/ra-emacs-lsp.el
Represent unknown types as {unknown} instead of [unknown]
[rust.git] / editors / emacs / ra-emacs-lsp.el
1 ;;; ra-emacs-lsp.el --- Rust analyzer emacs bindings for emacs-lsp -*- lexical-binding: t; -*-
2 ;;; Code:
3
4 (require 'lsp)
5 (require 'dash)
6 (require 'ht)
7
8 ;; This currently
9 ;;  - sets up rust-analyzer with emacs-lsp, giving
10 ;;    - code actions
11 ;;    - completion (use company-lsp for proper snippet support)
12 ;;    - imenu support
13 ;;    - on-type formatting
14 ;;    - 'hover' type information & documentation (with lsp-ui)
15 ;;  - implements source changes (for code actions etc.), except for file system changes
16 ;;  - implements joinLines (you need to bind rust-analyzer-join-lines to a key)
17 ;;  - implements extendSelection (either bind rust-analyzer-extend-selection to a key, or use expand-region)
18
19 ;; What's missing:
20 ;;  - file system changes in apply-source-change
21 ;;  - semantic highlighting
22 ;;  - onEnter, parentModule, findMatchingBrace
23 ;;  - runnables
24 ;;  - the debugging commands (syntaxTree and analyzerStatus)
25 ;;  - lsp-ui doesn't interpret the markdown we return currently and instead displays it raw (https://github.com/emacs-lsp/lsp-ui/issues/220 )
26 ;;  - more
27
28 ;; Also, there's a problem with company-lsp's caching being too eager, sometimes
29 ;; resulting in outdated completions.
30
31 (defcustom rust-analyzer-command '("ra_lsp_server")
32   ""
33   :type '(repeat (string)))
34
35 (defconst rust-analyzer--notification-handlers
36   '(("rust-analyzer/publishDecorations" . (lambda (_w _p)))))
37
38 (defconst rust-analyzer--action-handlers
39   '(("rust-analyzer.applySourceChange" .
40      (lambda (p) (rust-analyzer--apply-source-change-command p)))))
41
42 (defun rust-analyzer--uri-filename (text-document)
43   (lsp--uri-to-path (gethash "uri" text-document)))
44
45 (defun rust-analyzer--goto-lsp-loc (loc)
46   (-let (((&hash "line" "character") loc))
47     (goto-line (1+ line))
48     (move-to-column character)))
49
50 (defun rust-analyzer--apply-text-document-edit (edit)
51   "Like lsp--apply-text-document-edit, but it allows nil version."
52   (let* ((ident (gethash "textDocument" edit))
53          (filename (rust-analyzer--uri-filename ident))
54          (version (gethash "version" ident)))
55     (with-current-buffer (find-file-noselect filename)
56       (when (or (not version) (= version (lsp--cur-file-version)))
57         (lsp--apply-text-edits (gethash "edits" edit))))))
58
59 (defun rust-analyzer--apply-source-change (data)
60   ;; TODO fileSystemEdits
61   (--each (-> data (ht-get "workspaceEdit") (ht-get "documentChanges"))
62     (rust-analyzer--apply-text-document-edit it))
63   (-when-let (cursor-position (ht-get data "cursorPosition"))
64     (let ((filename (rust-analyzer--uri-filename (ht-get cursor-position "textDocument")))
65           (position (ht-get cursor-position "position")))
66       (find-file filename)
67       (rust-analyzer--goto-lsp-loc position)
68       )))
69
70 (defun rust-analyzer--apply-source-change-command (p)
71   (let ((data (-> p (ht-get "arguments") (car))))
72     (rust-analyzer--apply-source-change data)))
73
74 (lsp-register-client
75  (make-lsp-client
76   :new-connection (lsp-stdio-connection (lambda () rust-analyzer-command))
77   :notification-handlers (ht<-alist rust-analyzer--notification-handlers)
78   :action-handlers (ht<-alist rust-analyzer--action-handlers)
79   :major-modes '(rust-mode)
80   :ignore-messages nil
81   :server-id 'rust-analyzer))
82
83 (with-eval-after-load 'company-lsp
84   ;; company-lsp provides a snippet handler for rust by default that adds () after function calls, which RA does better
85   (setq company-lsp--snippet-functions (assq-delete-all "rust" company-lsp--snippet-functions)))
86
87 ;; join lines
88
89 (defun rust-analyzer--join-lines-params ()
90   "Join lines params."
91   (list :textDocument (lsp--text-document-identifier)
92         :range (if (use-region-p)
93                    (lsp--region-to-range (region-beginning) (region-end))
94                  (lsp--region-to-range (point) (point)))))
95
96 (defun rust-analyzer-join-lines ()
97   (interactive)
98   (->
99    (lsp-send-request (lsp-make-request "rust-analyzer/joinLines"
100                                        (rust-analyzer--join-lines-params)))
101    (rust-analyzer--apply-source-change)))
102
103 ;; extend selection
104
105 (defun rust-analyzer-extend-selection ()
106   (interactive)
107   (-let (((&hash "start" "end") (rust-analyzer--extend-selection)))
108     (rust-analyzer--goto-lsp-loc start)
109     (set-mark (point))
110     (rust-analyzer--goto-lsp-loc end)
111     (exchange-point-and-mark)))
112
113 (defun rust-analyzer--extend-selection-params ()
114   "Extend selection params."
115   (list :textDocument (lsp--text-document-identifier)
116         :selections
117         (vector
118          (if (use-region-p)
119              (lsp--region-to-range (region-beginning) (region-end))
120            (lsp--region-to-range (point) (point))))))
121
122 (defun rust-analyzer--extend-selection ()
123   (->
124    (lsp-send-request
125     (lsp-make-request
126      "rust-analyzer/extendSelection"
127      (rust-analyzer--extend-selection-params)))
128    (ht-get "selections")
129    (car)))
130
131 (defun rust-analyzer--add-er-expansion ()
132   (make-variable-buffer-local 'er/try-expand-list)
133   (setq er/try-expand-list (append
134                             er/try-expand-list
135                             '(rust-analyzer-extend-selection))))
136
137 (with-eval-after-load 'expand-region
138   (add-hook 'rust-mode-hook 'rust-analyzer--add-er-expansion))
139
140 ;; runnables
141 (defvar rust-analyzer--last-runnable)
142
143 (defun rust-analyzer--runnables-params ()
144   (list :textDocument (lsp--text-document-identifier)
145         :position (lsp--cur-position)))
146
147 (defun rust-analyzer--runnables ()
148   (lsp-send-request (lsp-make-request "rust-analyzer/runnables"
149                                       (rust-analyzer--runnables-params))))
150
151 (defun rust-analyzer--select-runnable ()
152   (lsp--completing-read
153    "Select runnable:"
154    (if rust-analyzer--last-runnable
155        (cons rust-analyzer--last-runnable (rust-analyzer--runnables))
156        (rust-analyzer--runnables))
157    (-lambda ((&hash "label")) label)))
158
159 (defun rust-analyzer-run (runnable)
160   (interactive (list (rust-analyzer--select-runnable)))
161   (-let (((&hash "env" "bin" "args" "label") runnable))
162     (compilation-start
163      (string-join (cons bin args) " ")
164      ;; cargo-process-mode is nice, but try to work without it...
165      (if (functionp 'cargo-process-mode) 'cargo-process-mode nil)
166      (lambda (_) (concat "*" label "*")))
167     (setq rust-analyzer--last-runnable runnable)))
168
169 (defun rust-analyzer-rerun (&optional runnable)
170   (interactive (list (or rust-analyzer--last-runnable
171                          (rust-analyzer--select-runnable))))
172   (rust-analyzer-run (or runnable rust-analyzer--last-runnable)))
173
174 (provide 'ra-emacs-lsp)
175 ;;; ra-emacs-lsp.el ends here