aboutsummaryrefslogtreecommitdiff
path: root/editors/emacs/ra-emacs-lsp.el
blob: b13068ee24bfae3373423eeefb910962503240a7 (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
;;; 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))

(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)))

(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)))

;; 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))

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