-
Notifications
You must be signed in to change notification settings - Fork 90
/
omnisharp.el
626 lines (536 loc) · 24.3 KB
/
omnisharp.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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
;;; omnisharp.el --- Omnicompletion (intellisense) and more for C# -*- lexical-binding: t; -*-
;; Copyright (C) 2013-2018 Mika Vilpas and others (GPLv3)
;; Author: Mika Vilpas and others
;; Version: 4.2
;; License: GNU General Public License version 3, or (at your option) any later version
;; Url: https://github.com/Omnisharp/omnisharp-emacs
;; Package-Requires: ((emacs "24.4") (flycheck "30") (dash "2.12.0") (auto-complete "1.4") (popup "0.5.1") (csharp-mode "0.8.7") (cl-lib "0.5") (s "1.10.0") (f "0.19.0"))
;; Keywords: languages csharp c# IDE auto-complete intellisense
;; This file 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, or (at your option)
;; any later version.
;; This file 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:
;; omnisharp-emacs is a port of the awesome OmniSharp server to the
;; Emacs text editor. It provides IDE-like features for editing files
;; in C# solutions in Emacs, provided by an OmniSharp server instance
;; that works in the background.
;;
;; See the project home page for more information.
(require 'cl)
(require 'cl-lib)
(require 'csharp-mode)
(require 'json)
(require 'files)
(require 'ido)
(require 'thingatpt)
(require 'dash)
(require 'compile)
(require 'dired)
(require 'popup)
(require 'etags)
(require 'flycheck)
(require 's)
(require 'f)
(require 'omnisharp-settings)
(require 'omnisharp-server-management)
(require 'omnisharp-utils)
(require 'omnisharp-http-utils)
(require 'omnisharp-server-actions)
(require 'omnisharp-auto-complete-actions)
(require 'omnisharp-current-symbol-actions)
(require 'omnisharp-navigation-actions)
(require 'omnisharp-helm-integration)
(require 'omnisharp-solution-actions)
(require 'omnisharp-format-actions)
(require 'omnisharp-server-installation)
(require 'omnisharp-code-structure)
(require 'omnisharp-unit-test-actions)
;;; Code:
;;;###autoload
(define-minor-mode omnisharp-mode
"Omnicompletion (intellisense) and more for C# using an OmniSharp
server backend."
:lighter " omnisharp"
:global nil
:keymap omnisharp-mode-map
(omnisharp--init-imenu-support)
(omnisharp--init-eldoc-support)
(omnisharp--attempt-to-start-server-for-buffer)
;; These are selected automatically when flycheck is enabled
(add-to-list 'flycheck-checkers 'csharp-omnisharp-codecheck))
(defun omnisharp--init-imenu-support ()
(when omnisharp-imenu-support
(if omnisharp-mode
(progn
(setq imenu-create-index-function 'omnisharp-imenu-create-index)
(imenu-add-menubar-index))
(setq imenu-create-index-function 'imenu-default-create-index-function))))
(defun omnisharp--init-eldoc-support ()
(when omnisharp-eldoc-support
(when omnisharp-mode
(make-local-variable 'eldoc-documentation-function)
(setq eldoc-documentation-function 'omnisharp-eldoc-function))))
;;
;; below are all the interactive functions that are to be available via autoload
;;
;; (autoloaded f-ns should go in omnisharp.el in order to make
;; internal file dependencies easier to manage when autoloading)
;;
;;;###autoload
(defun omnisharp-start-omnisharp-server (&optional no-autodetect)
"Starts an OmniSharp server for a given path to a project or solution file"
(interactive "P")
(omnisharp--start-omnisharp-server no-autodetect))
;;;###autoload
(defun omnisharp-stop-server ()
"Stops Omnisharp server if running."
(interactive)
(omnisharp--stop-server))
;;;###autoload
(defun omnisharp-reload-solution ()
"Restarts omnisharp server on solution last loaded"
(interactive)
(omnisharp--reload-solution))
;;;###autoload
(defun omnisharp-check-alive-status ()
"Shows a message to the user describing whether the
OmniSharpServer process specified in the current configuration is
alive.
\"Alive\" means it is running and not stuck. It also means the connection
to the server is functional - I.e. The user has the correct host and
port specified."
(interactive)
(omnisharp--check-alive-status))
;;;###autoload
(defun omnisharp-check-ready-status ()
"Shows a message to the user describing whether the
OmniSharpServer process specified in the current configuration has
finished loading the solution."
(interactive)
(omnisharp--check-ready-status))
;;;###autoload
(defun omnisharp-install-server (reinstall)
"Installs OmniSharp server locally into ~/.emacs/cache/omnisharp/server/$(version)"
(interactive "P")
(omnisharp--install-server reinstall))
;;;###autoload
(defun company-omnisharp (command &optional arg &rest ignored)
(interactive '(interactive))
"`company-mode' completion back-end using OmniSharp."
(cl-case command
(interactive (company-begin-backend 'company-omnisharp))
(prefix (when (and (bound-and-true-p omnisharp-mode)
(not (company-in-string-or-comment)))
(omnisharp-company--prefix)))
(candidates (omnisharp--get-company-candidates arg))
;; because "" doesn't return everything, and we don't cache if we're handling the filtering
(no-cache (or (equal arg "")
(not (eq omnisharp-company-match-type 'company-match-simple))))
(match (if (eq omnisharp-company-match-type 'company-match-simple)
nil
0))
(annotation (omnisharp--company-annotation arg))
(meta (omnisharp--get-company-candidate-data arg 'DisplayText))
(require-match 'never)
(doc-buffer (let ((doc-buffer (company-doc-buffer
(omnisharp--get-company-candidate-data
arg 'Description))))
(with-current-buffer doc-buffer
(visual-line-mode))
doc-buffer))
(ignore-case omnisharp-company-ignore-case)
(sorted (if (eq omnisharp-company-match-type 'company-match-simple)
(not omnisharp-company-sort-results)
t))
;; Check to see if we need to do any templating
(post-completion (let* ((json-result (get-text-property 0 'omnisharp-item arg))
(allow-templating (get-text-property 0 'omnisharp-allow-templating arg)))
(omnisharp--tag-text-with-completion-info arg json-result)
(when allow-templating
;; Do yasnippet completion
(if (and omnisharp-company-template-use-yasnippet (boundp 'yas-minor-mode) yas-minor-mode)
(-when-let (method-snippet (omnisharp--completion-result-item-get-method-snippet
json-result))
(omnisharp--snippet-templatify arg method-snippet json-result))
;; Fallback on company completion but make sure company-template is loaded.
;; Do it here because company-mode is optional
(require 'company-template)
(let ((method-base (omnisharp--get-method-base json-result)))
(when (and method-base
(string-match-p "([^)]" method-base))
(company-template-c-like-templatify method-base)))))))))
;;
;; easymenu
;;
(easy-menu-define omnisharp-mode-menu omnisharp-mode-map
"Menu for omnisharp-mode"
'("OmniSharp"
("Auto-complete"
["at point" omnisharp-auto-complete]
["Add . and complete members" omnisharp-add-dot-and-auto-complete]
["Show last result" omnisharp-show-last-auto-complete-result]
["Show overloads at point" omnisharp-show-overloads-at-point])
("Navigate to.."
["Definition at point" omnisharp-go-to-definition]
["Current file member" omnisharp-navigate-to-current-file-member]
["Solution member" omnisharp-navigate-to-solution-member]
["File in solution" omnisharp-navigate-to-solution-file]
["Region in current file" omnisharp-navigate-to-region])
("OmniSharp server"
["Start OmniSharp server" omnisharp-start-omnisharp-server]
["Stop OmniSharp server" omnisharp-stop-server]
["Check alive status" omnisharp-check-alive-status]
["Check ready status" omnisharp-check-ready-status])
("Current symbol"
["Show type" omnisharp-current-type-information]
["Show documentation" omnisharp-current-type-documentation]
["Show type and add it to kill ring" omnisharp-current-type-information-to-kill-ring]
["Find usages" omnisharp-find-usages]
["Find usages with ido" omnisharp-find-usages-with-ido]
["Find implementations" omnisharp-find-implementations]
["Find implementations with ido" omnisharp-find-implementations-with-ido]
["Rename" omnisharp-rename])
("Solution actions"
["Start syntax check" flycheck-mode]
["Fix code issue at point" omnisharp-fix-code-issue-at-point]
["Run code format on current buffer" omnisharp-code-format-entire-file])
["Run contextual code action / refactoring at point" omnisharp-run-code-action-refactoring]))
(defvar omnisharp--eldoc-fontification-buffer-name " * OmniSharp : Eldoc Fontification *"
"The name of the buffer that is used to fontify eldoc strings.")
(defun omnisharp--region-start-line ()
(when mark-active
(save-excursion
(goto-char (region-beginning))
(line-number-at-pos))))
(defun omnisharp--goto-end-of-region ()
"evil-mode has its own Vim-like concept of the region. A visual
line selection in evil-mode reports the end column to be 0 in
some cases. Work around this."
(when mark-active
(if (and (boundp 'evil-visual-end)
evil-visual-end
;; at this point we know the user is using evil-mode.
;; It's possible to select vanilla emacs regions even
;; when using evil-mode, so make sure the user has
;; selected the region using evil-visual-state
(fboundp 'evil-visual-state-p)
(evil-visual-state-p))
(-let (((_start end _selection-type) (evil-visual-range)))
;; Point moves to the next line when it's at the very last
;; character of the line, for some reason. So move it back.
(goto-char end)
(backward-char))
(goto-char (region-end)))))
(defun omnisharp--region-end-line ()
(when mark-active
(save-excursion (omnisharp--goto-end-of-region)
(line-number-at-pos))))
(defun omnisharp--region-start-column ()
(when mark-active
(save-excursion
(goto-char (region-beginning))
(omnisharp--current-column))))
(defun omnisharp--region-end-column ()
(when mark-active
(save-excursion
(omnisharp--goto-end-of-region)
(omnisharp--current-column))))
(defun omnisharp--completion-result-item-get-completion-text (item)
(cdr (assoc 'CompletionText item)))
(defun omnisharp--completion-result-item-get-display-text (item)
(cdr (assoc 'DisplayText item)))
(defun omnisharp--completion-result-item-get-method-header (item)
(cdr (assoc 'MethodHeader item)))
(defun omnisharp--completion-result-item-get-method-snippet (item)
(cdr (assoc 'Snippet item)))
(defun omnisharp--completion-result-get-item (json-alist type)
(cdr (assoc type json-alist)))
(defun omnisharp--get-max-item-length (completions)
"Returns the length of the longest completion in 'completions'."
(if (null completions)
0
(cl-reduce 'max (mapcar 'length completions))))
(defun omnisharp--get-request-object ()
"Construct a Request object based on the current buffer contents."
(let* ((line-number (number-to-string (line-number-at-pos)))
(column-number (number-to-string (omnisharp--current-column)))
(buffer-contents (if (boundp 'omnisharp--metadata-source)
nil
(omnisharp--get-current-buffer-contents)))
(filename-tmp (or buffer-file-name ""))
(params `((Line . ,line-number)
(Column . ,column-number)
(Buffer . ,buffer-contents))))
(cond
((boundp 'omnisharp--metadata-source)
(cons `(FileName . ,omnisharp--metadata-source)
params))
((/= 0 (length filename-tmp))
(cons (omnisharp--to-filename filename-tmp)
params))
(t
params))))
(defun omnisharp--get-typelookup-request-object ()
"Construct a Request object for typelookup endpoint based on the current buffer contents."
(append
'((IncludeDocumentation . t))
(omnisharp--get-request-object)))
(defun omnisharp--get-request-object-for-emacs-side-use ()
"Gets a Request class that can be only handled safely inside
Emacs. This should not be transferred to the server backend - it might
not work on all platforms."
(let* ((line-number (line-number-at-pos))
(column-number (omnisharp--current-column))
(buffer-contents (omnisharp--get-current-buffer-contents))
(filename-tmp (or buffer-file-name ""))
(params `((Line . ,line-number)
(Column . ,column-number)
(Buffer . ,buffer-contents))))
(if (/= 0 (length filename-tmp))
(cons (omnisharp--to-filename filename-tmp)
params)
params)))
(defun omnisharp-go-to-file-line-and-column (json-result
&optional other-window
buffer)
"Open file :FileName at :Line and :Column. If filename is not given,
defaults to the current file. This function works for a
QuickFix class json result.
Switches to BUFFER instead of :FileName when buffer is set."
(omnisharp-go-to-file-line-and-column-worker
(cdr (assoc 'Line json-result))
(- (cdr (assoc 'Column json-result)) 1)
(omnisharp--get-filename json-result)
other-window
nil
buffer))
(defun omnisharp--go-to-line-and-column (line column)
(goto-char (point-min))
(forward-line (1- line))
(forward-char (max 0 column)))
(defun omnisharp-go-to-file-line-and-column-worker (line
column
&optional filename
other-window
dont-save-old-pos
buffer)
"Open filename at line and column. Switches to BUFFER if provided,
otherwise defaults to the current file if filename is not given.
Saves the current location into the tag ring so that the user may
return with (pop-tag-mark).
If DONT-SAVE-OLD-POS is specified, will not save current position to
find-tag-marker-ring. This is so this function may be used without
messing with the ring."
(let ((position-before-jumping (point-marker)))
(when (or buffer filename)
(omnisharp--find-file-possibly-in-other-window (or buffer filename)
other-window))
;; calling goto-line directly results in a compiler warning.
(omnisharp--go-to-line-and-column line column)
(unless dont-save-old-pos
(omnisharp--save-position-to-find-tag-marker-ring
position-before-jumping)
(omnisharp--show-last-buffer-position-saved-message
(buffer-file-name
(marker-buffer position-before-jumping))))))
(defun omnisharp--show-last-buffer-position-saved-message
(&optional file-name)
"Notifies the user that the previous buffer position has been saved
with a message in the minibuffer. If FILE-NAME is given, shows that as
the file. Otherwise uses the current file name."
(message "Previous position in %s saved. Go back with (pop-tag-mark)."
(or file-name
(buffer-file-name))))
(defun omnisharp--save-position-to-find-tag-marker-ring
(&optional marker)
"Record position in find-tag-marker-ring. If MARKER is non-nil,
record that position. Otherwise record the current position."
(setq marker (or marker (point-marker)))
(ring-insert find-tag-marker-ring marker))
(defun omnisharp--find-file-possibly-in-other-window
(file &optional other-window)
"Open a buffer editing FILE. If no buffer for that filename
exists, a new one is created.
If the optional argument OTHER-WINDOW is non-nil, uses another
window.
FILE can be a buffer in which case that buffer is selected."
(cond
((or (bufferp file)
(omnisharp--buffer-exists-for-file-name file))
(let ((target-buffer-to-switch-to
(if (bufferp file)
file
(--first (string= (buffer-file-name it)
file)
(buffer-list)))))
(if other-window
(pop-to-buffer target-buffer-to-switch-to)
(pop-to-buffer-same-window target-buffer-to-switch-to))))
(t ; no buffer for this file exists yet
(funcall (if other-window
'find-file-other-window
'find-file)
file))))
(defun omnisharp--vector-to-list (vector)
(append vector nil))
(defun omnisharp--popup-to-ido ()
"When in a popup menu with autocomplete suggestions, calling this
function will close the popup and open an ido prompt instead.
Note that currently this will leave the popup menu active even when
the user selects a completion and the completion is inserted."
(interactive) ; required. Otherwise call to this is silently ignored
;; TODO how to check if popup is active?
(omnisharp--auto-complete-display-function-ido
omnisharp--last-buffer-specific-auto-complete-result))
(defun omnisharp--flycheck-start (checker callback)
"Start an OmniSharp syntax check with CHECKER.
CALLBACK is the status callback passed by Flycheck."
;; Put the current buffer into the closure environment so that we have access
;; to it later.
(let ((buffer (current-buffer)))
(omnisharp--send-command-to-server
"codecheck"
(omnisharp--get-request-object)
(lambda (response)
(let ((errors (omnisharp--flycheck-error-parser response checker buffer)))
(funcall callback 'finished (delq nil errors)))))))
(flycheck-define-generic-checker 'csharp-omnisharp-codecheck
"A csharp source syntax checker using the OmniSharp server process
running in the background"
:start #'omnisharp--flycheck-start
:modes '(csharp-mode)
:predicate (lambda () (and omnisharp-mode
omnisharp--server-info
(not (boundp 'omnisharp--metadata-source)))))
(defun omnisharp--flycheck-error-parser (response checker buffer)
"Takes a QuickFixResponse result. Returns flycheck errors created based on the
locations in the json."
(->> (omnisharp--vector-to-list
(cdr (assoc 'QuickFixes response)))
(mapcar (lambda (it)
(flycheck-error-new
:buffer buffer
:checker checker
:filename (omnisharp--get-filename it)
:line (cdr (assoc 'Line it))
:column (cdr (assoc 'Column it))
:message (cdr (assoc 'Text it))
:level (pcase (cdr (assoc 'LogLevel it))
("Warning" 'warning)
("Hidden" 'info)
(_ 'error)))))))
(defun omnisharp--imenu-make-marker (element)
"Takes a QuickCheck element and returns the position of the
cursor at that location"
(let* ((element-line (cdr (assoc 'Line element)))
(element-column (cdr (assoc 'Column element)))
(element-filename (omnisharp--get-filename element)))
(save-excursion
(omnisharp-go-to-file-line-and-column-worker
element-line
element-column
element-filename
nil ; other-window
;; dont-save-old-pos
t)
(point-marker))))
(defun omnisharp-imenu-create-index ()
"Imenu callback function - returns an alist of ((member-name . position))"
(interactive)
(condition-case nil
(let (result)
(omnisharp--send-command-to-server-sync
"currentfilemembersasflat"
(omnisharp--get-request-object)
(lambda (quickfixes)
(setq result
(-map (lambda (quickfix-alist)
(cons (cdr (assoc 'Text quickfix-alist))
(omnisharp--imenu-make-marker quickfix-alist)))
quickfixes))))
result)
(error nil)))
(defun omnisharp--jump-to-enclosing-func ()
"Jumps to the closing brace of the current function definition"
(interactive)
(let ((start-point (point))
(found-point (point))
(found-start nil))
(save-excursion
(let ((test-point (point)))
(while (not found-start)
(search-backward-regexp "(\\|;\\|{")
(cond ((eq (point) test-point)
(setq found-start t))
((looking-at-p "(")
(setq test-point (point))
;; forward-sexp will throw an error if the sexp is unbalanced
(condition-case nil
(forward-sexp)
(error nil))
(when (> (point) start-point)
(setq found-point test-point)
(setq found-start t))
(goto-char test-point))
(t (setq found-start t))))))
(goto-char found-point)))
(defun omnisharp--get-eldoc-fontification-buffer ()
(let ((buffer (get-buffer omnisharp--eldoc-fontification-buffer-name)))
(if (buffer-live-p buffer)
buffer
(with-current-buffer (generate-new-buffer omnisharp--eldoc-fontification-buffer-name)
(ignore-errors
(let ((csharp-mode-hook nil))
(csharp-mode)))
(current-buffer)))))
(defun omnisharp--eldoc-fontify-string (str)
(with-current-buffer (omnisharp--get-eldoc-fontification-buffer)
(delete-region (point-min) (point-max))
(font-lock-fontify-region (point) (progn (insert str ";") (point)))
(buffer-substring (point-min) (1- (point-max)))))
(defun omnisharp-eldoc-function ()
"Returns a doc string appropriate for the current context.
If point is on an empty char, it looks for data on any previous completions.
Otherwise, returns nil."
(unless (equal nil omnisharp--server-info)
(condition-case nil
(if (not (looking-at-p " "))
(progn
(condition-case nil
(omnisharp--send-command-to-server
"typelookup"
(omnisharp--get-typelookup-request-object)
(lambda (response)
(let* ((current-type-information
(omnisharp--completion-result-get-item response 'Type))
(current-type-documentation
(string-trim-right
(or (omnisharp--completion-result-get-item response 'Documentation)
"")))
(have-type (and current-type-information (not (string= "" current-type-information))))
(have-doc (and current-type-documentation (not (string= "" current-type-documentation))))
(message-to-show (concat (if current-type-information (omnisharp--eldoc-fontify-string current-type-information))
(if (and have-type have-doc) "\n\n")
current-type-documentation)))
(if (or have-type have-doc)
(eldoc-message message-to-show)))))
(error nil (ignore)))
nil))
(error nil (ignore)))))
(add-to-list 'compilation-error-regexp-alist
'(" in \\(.+\\):\\([1-9][0-9]+\\)" 1 2))
;; nunit-console.exe on windows uses this format
(add-to-list 'compilation-error-regexp-alist
'(" in \\(.+\\):line \\([0-9]+\\)" 1 2))
;; dotnet test with xunit project
;; [xUnit.net 00:00:00.6080370] /TestProject/UnitTest1.cs(15,0): at TestProject.UnitTest1.Test1()
(add-to-list 'compilation-error-regexp-alist '("\\[xUnit.net .*\\] +\\(.*\\)(\\([[:digit:]]+\\),\\([[:digit:]]+\\))" 1 2 3))
(provide 'omnisharp)
;;; omnisharp.el ends here