-
Notifications
You must be signed in to change notification settings - Fork 16
/
company-sourcekit.el
211 lines (183 loc) · 7.66 KB
/
company-sourcekit.el
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
199
200
201
202
203
204
205
206
207
208
209
210
211
;;; company-sourcekit.el --- company-mode completion backend for SourceKit -*- lexical-binding: t; -*-
;; Copyright (C) 2015 Nathan Kot
;; Author: Nathan Kot <[email protected]>
;; URL: https://github.com/nathankot/company-sourcekit
;; Keywords: abbrev
;; Version: 0.2.0
;; Package-Requires: ((emacs "24.3") (company "0.8.12") (dash "2.18.0") (sourcekit "0.2.0"))
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; A company-mode backend for swift projects. It communicates with SourceKit
;; via sourcekittendaemon in order to obtain completions for Xcode projects.
;;; Code:
(require 'company)
(require 'cl-lib)
(require 'json)
(require 'dash)
(require 'sourcekit)
(defgroup company-sourcekit nil
"Completion backend that uses sourcekit"
:group 'company)
(defcustom company-sourcekit-use-yasnippet
(fboundp 'yas-minor-mode)
"Should Yasnippet be used for completion expansion."
:type 'boolean
:group 'company-sourcekit)
(defcustom company-sourcekit-verbose nil
"Should log with verbosity to the messages buffer."
:type 'boolean
:group 'company-sourcekit)
;;;###autoload
(defun company-sourcekit (command &optional arg &rest ignored)
"Company backend for swift using sourcekitten."
(interactive (list 'interactive))
(cl-case command
(interactive (company-begin-backend 'company-sourcekit))
(init (progn
(unless sourcekit-sourcekittendaemon-executable
(error "[company-sourcekit] sourcekittendaemon not found in PATH"))
(if (eq (sourcekit-project) 'unknown)
(error "[company-sourcekit] *.xcodeproj not found in directory tree"))))
(sorted t)
(prefix (company-sourcekit--prefix))
(candidates (cons :async (lambda (cb) (company-sourcekit--candidates arg cb))))
(meta (company-sourcekit--meta arg))
(annotation (company-sourcekit--annotation arg))
(post-completion (company-sourcekit--post-completion arg))))
;;; Private:
(defvar-local company-sourcekit--tmp-file 'unknown)
(defun company-sourcekit--tmp-file ()
(when (eq company-sourcekit--tmp-file 'unknown)
(setq company-sourcekit--tmp-file (make-temp-file "sourcekitten")))
company-sourcekit--tmp-file)
(defun company-sourcekit--prefix ()
"In our case, the prefix acts as a cache key for company-mode.
It never actually gets sent to the completion engine."
(and
(eq major-mode 'swift-mode)
(not (company-in-string-or-comment))
(or
;; Fetch prefix during import statements:
;;
;; Given: "import |"
;; Prefix: ""
;; Offset: 7
;;
;; Given: "import Found|"
;; Prefix: "Found"
;; Offset: 7
(-when-let* ((x (company-grab-symbol-cons "import ")) (_ (listp x))) x)
;; Fetch prefix for method calls:
;;
;; Given: "self.|"
;; Prefix: ""
;; Offset: 5
;;
;; Given: "self.hel|"
;; Prefix: "hel"
;; Offset: 5
(let ((r (company-grab-symbol-cons "\\.")))
(when (consp r) r))
;; Fetch prefix for function calls:
;;
;; Given: "CGRect(|)"
;; Prefix: ""
;; Offset: 7
;;
;; Given: "CGRect(x:|)"
;; Prefix: "x:"
;; Offset: 7
(-if-let (x (company-grab "\_*(\\([\w\_:]*?\\)" 1 (line-beginning-position)))
(cons x t))
;; Fetch prefix for symbols:
;;
;; Given: "let r = CGRe|"
;; Prefix: ""
;; Offset: 12
;;
;; Given: "let r = CGRec|"
;; Prefix: ""
;; Offset: 13
(-if-let (x (company-grab-symbol))
(when (> (length x) 0) (cons "" t))))))
(defun company-sourcekit--meta (candidate)
"Gets the meta for the completion candidate."
(get-text-property 0 'description candidate))
(defun company-sourcekit--annotation (candidate)
"Gets the type of the completion candidate."
(format " %s" (get-text-property 0 'type candidate)))
(defun company-sourcekit--candidates (prefix callback)
"Use sourcekitten to get a list of completion candidates."
(sourcekit-with-daemon-for-project (sourcekit-project)
(lambda (port)
(if (not port) (funcall callback nil)
(let* ((tmpfile (company-sourcekit--tmp-file))
(offset (- (position-bytes (point)) (string-bytes prefix) (position-bytes (point-min)))))
(write-region (point-min) (point-max) tmpfile nil 'silent)
(when company-sourcekit-verbose
(message "[company-sourcekit] prefix: `%s`, file: %s, offset: %d" prefix tmpfile offset))
;; Make HTTP request to the sourcekittendaemon, asynchronously
(sourcekit-query port "/complete"
`(("X-Offset" . ,(number-to-string offset)) ("X-Path" . ,tmpfile))
(company-sourcekit--make-callback callback)))))))
(defun company-sourcekit--make-callback (callback)
"The handler for process output."
(lambda (json)
(let ((completions (-filter
(lambda (candidate) (eq 0 (string-match-p company-prefix candidate)))
(company-sourcekit--process-json json))))
(when company-sourcekit-verbose
(message "[company-sourcekit] sending results to company"))
(funcall callback completions))))
(defun company-sourcekit--process-json (return-json)
"Given json returned from sourcekitten, turn it into a list compatible with company-mode"
(append (mapcar
(lambda (l)
(let ((name (cdr (assoc 'name l)))
(desc (cdr (assoc 'descriptionKey l)))
(src (cdr (assoc 'sourcetext l)))
(type (cdr (assoc 'typeName l))))
(propertize (company-sourcekit--normalize-source-text src)
'sourcetext src
'description desc
'type type)))
return-json) nil))
(declare-function yas-expand-snippet "yasnippet")
(defun company-sourcekit--post-completion (completed)
"Post completion - expand yasnippet if necessary"
(when company-sourcekit-use-yasnippet
(when company-sourcekit-verbose (message "[company-sourcekit] expanding yasnippet template"))
(let ((template (company-sourcekit--build-yasnippet (get-text-property 0 'sourcetext completed))))
(when company-sourcekit-verbose (message "[company-sourcekit] %s" template))
(yas-expand-snippet template (- (point) (length completed)) (point)))))
(defun company-sourcekit--normalize-source-text (sourcetext)
"Make a more readable completion candidate out of one with placeholders."
(replace-regexp-in-string
"<#T##\\(.*?\\)#>"
(lambda (str)
;; <#T##Int#> - No label, argument only
(save-match-data
(string-match "<#T##\\(.*?\\)#>" str)
(format "%s" (car (split-string (match-string 1 str) "#")))))
sourcetext))
(defun company-sourcekit--build-yasnippet (sourcetext)
"Build a yasnippet-compatible snippet from the given source text"
(replace-regexp-in-string
"<#T##\\(.*?\\)#>"
(lambda (str)
;; <#T##Int#> - No label, argument only
(save-match-data
(string-match "<#T##\\(.*?\\)#>" str)
(format "${%s}" (car (split-string (match-string 1 str) "#")))))
sourcetext))
(provide 'company-sourcekit)
;;; company-sourcekit.el ends here