aboutsummaryrefslogtreecommitdiff
path: root/editors/emacs/ra-emacs-lsp.el
blob: 4af65c7f022daa547aedc2e5b4cf5608f3685666 (plain)
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
;;; ra-emacs-lsp.el --- Rust analyzer emacs bindings for emacs-lsp -*- lexical-binding: t; -*-
;;; Code:

(require 'lsp)
(require 'dash)
(require 'ht)

;; This currently
;;  - sets up rust-analyzer with emacs-lsp, giving
;;    - code actions
;;    - completion (use company-lsp for proper snippet support)
;;    - imenu support
;;    - on-type formatting
;;    - 'hover' type information & documentation (with lsp-ui)
;;  - implements source changes (for code actions etc.), except for file system changes
;;  - implements joinLines (you need to bind rust-analyzer-join-lines to a key)
;;  - implements extendSelection (either bind rust-analyzer-extend-selection to a key, or use expand-region)

;; What's missing:
;;  - file system changes in apply-source-change
;;  - semantic highlighting
;;  - onEnter, parentModule, findMatchingBrace
;;  - runnables
;;  - the debugging commands (syntaxTree and analyzerStatus)
;;  - lsp-ui doesn't interpret the markdown we return currently and instead displays it raw (https://github.com/emacs-lsp/lsp-ui/issues/220 )
;;  - more

;; Also, there's a problem with company-lsp's caching being too eager, sometimes
;; resulting in outdated completions.

(defcustom rust-analyzer-command '("ra_lsp_server")
  ""
  :type '(repeat (string)))

(defconst rust-analyzer--notification-handlers
  '(("rust-analyzer/publishDecorations" . (lambda (_w _p)))))

(defconst rust-analyzer--action-handlers
  '(("rust-analyzer.applySourceChange" .
     (lambda (p) (rust-analyzer--apply-source-change-command p)))))

(defun rust-analyzer--uri-filename (text-document)
  (lsp--uri-to-path (gethash "uri" text-document)))

(defun rust-analyzer--goto-lsp-loc (loc)
  (-let (((&hash "line" "character") loc))
    (goto-line (1+ line))
    (move-to-column character)))

(defun rust-analyzer--apply-text-document-edit (edit)
  "Like lsp--apply-text-document-edit, but it allows nil version."
  (let* ((ident (gethash "textDocument" edit))
         (filename (rust-analyzer--uri-filename ident))
         (version (gethash "version" ident)))
    (with-current-buffer (find-file-noselect filename)
      (when (or (not version) (= version (lsp--cur-file-version)))
        (lsp--apply-text-edits (gethash "edits" edit))))))

(defun rust-analyzer--apply-source-change (data)
  ;; TODO fileSystemEdits
  (--each (-> data (ht-get "workspaceEdit") (ht-get "documentChanges"))
    (rust-analyzer--apply-text-document-edit it))
  (-when-let (cursor-position (ht-get data "cursorPosition"))
    (let ((filename (rust-analyzer--uri-filename (ht-get cursor-position "textDocument")))
          (position (ht-get cursor-position "position")))
      (find-file filename)
      (rust-analyzer--goto-lsp-loc position)
      )))

(defun rust-analyzer--apply-source-change-command (p)
  (let ((data (-> p (ht-get "arguments") (car))))
    (rust-analyzer--apply-source-change data)))

(lsp-register-client
 (make-lsp-client
  :new-connection (lsp-stdio-connection (lambda () rust-analyzer-command))
  :notification-handlers (ht<-alist rust-analyzer--notification-handlers)
  :action-handlers (ht<-alist rust-analyzer--action-handlers)
  :major-modes '(rust-mode)
  :ignore-messages nil
  :server-id 'rust-analyzer))

(with-eval-after-load 'company-lsp
  ;; company-lsp provides a snippet handler for rust by default that adds () after function calls, which RA does better
  (setq company-lsp--snippet-functions (assq-delete-all "rust" company-lsp--snippet-functions)))

;; join lines

(defun rust-analyzer--join-lines-params ()
  "Join lines params."
  (list :textDocument (lsp--text-document-identifier)
        :range (if (use-region-p)
                   (lsp--region-to-range (region-beginning) (region-end))
                 (lsp--region-to-range (point) (point)))))

(defun rust-analyzer-join-lines ()
  (interactive)
  (->
   (lsp-send-request (lsp-make-request "rust-analyzer/joinLines"
                                       (rust-analyzer--join-lines-params)))
   (rust-analyzer--apply-source-change)))

;; extend selection

(defun rust-analyzer-extend-selection ()
  (interactive)
  (-let (((&hash "start" "end") (rust-analyzer--extend-selection)))
    (rust-analyzer--goto-lsp-loc start)
    (set-mark (point))
    (rust-analyzer--goto-lsp-loc end)
    (exchange-point-and-mark)))

(defun rust-analyzer--extend-selection-params ()
  "Extend selection params."
  (list :textDocument (lsp--text-document-identifier)
        :selections
        (vector
         (if (use-region-p)
             (lsp--region-to-range (region-beginning) (region-end))
           (lsp--region-to-range (point) (point))))))

(defun rust-analyzer--extend-selection ()
  (->
   (lsp-send-request
    (lsp-make-request
     "rust-analyzer/extendSelection"
     (rust-analyzer--extend-selection-params)))
   (ht-get "selections")
   (car)))

(defun rust-analyzer--add-er-expansion ()
  (make-variable-buffer-local 'er/try-expand-list)
  (setq er/try-expand-list (append
                            er/try-expand-list
                            '(rust-analyzer-extend-selection))))

(with-eval-after-load 'expand-region
  (add-hook 'rust-mode-hook 'rust-analyzer--add-er-expansion))

;; runnables
(defvar rust-analyzer--last-runnable)

(defun rust-analyzer--runnables-params ()
  (list :textDocument (lsp--text-document-identifier)
        :position (lsp--cur-position)))

(defun rust-analyzer--runnables ()
  (lsp-send-request (lsp-make-request "rust-analyzer/runnables"
                                      (rust-analyzer--runnables-params))))

(defun rust-analyzer--select-runnable ()
  (lsp--completing-read
   "Select runnable:"
   (if rust-analyzer--last-runnable
       (cons rust-analyzer--last-runnable (rust-analyzer--runnables))
       (rust-analyzer--runnables))
   (-lambda ((&hash "label")) label)))

(defun rust-analyzer-run (runnable)
  (interactive (list (rust-analyzer--select-runnable)))
  (-let (((&hash "env" "bin" "args" "label") runnable))
    (compilation-start
     (string-join (cons bin args) " ")
     ;; cargo-process-mode is nice, but try to work without it...
     (if (functionp 'cargo-process-mode) 'cargo-process-mode nil)
     (lambda (_) (concat "*" label "*")))
    (setq rust-analyzer--last-runnable runnable)))

(defun rust-analyzer-rerun (&optional runnable)
  (interactive (list (or rust-analyzer--last-runnable
                         (rust-analyzer--select-runnable))))
  (rust-analyzer-run (or runnable rust-analyzer--last-runnable)))

(provide 'ra-emacs-lsp)
;;; ra-emacs-lsp.el ends here