forked from nim-lang/nim-mode
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnim-suggest.el
871 lines (772 loc) · 32.7 KB
/
nim-suggest.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
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
;;; nim-suggest.el --- a plugin to use nimsuggest from Emacs -*- lexical-binding: t -*-
;; Description: A minor mode for Nim language tooling using nimsuggest
;; Author: Simon Hafner
;; Maintainer: Yuta Yamada <cokesboy"at"gmail.com>
;; Package-Requires: ((emacs "24.4") (epc "0.1.1"))
;; 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 2, 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; see the file COPYING. If not, write to
;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth
;; Floor, Boston, MA 02110-1301, USA.
;;; Commentary:
;; -- document is still work in progress --
;; Supporting features:
;; - Xref package (cross reference package for Emacs) -- from Emacs 25.1
;; - flycheck/flymake (on the fly linter)
;; - flycheck-nimsugget is other repo
;; - flymake-nimsuggest is only available if you use Emacs 26 or higher
;; - eldoc (help on the hover)
;; - company-mode (auto-completion)
;; TODO: write manual configuration, but basically you can find
;; `nimsuggest-mode-hook' what this package does though.
;;; Code:
(require 'nim-mode)
(require 'epc)
(require 'cl-lib)
(defconst nim--epc-keywords
;; Those names come from suggest.nim
'(:section ; sug, con, def, use, dus, highlight, outline
:symkind ; symKind
:qpath ; qualifiedPath
:file ; filePath
:forth ; type
:line ; min is 1 in suggest.nim
:column ; min is 0 in suggest.nim
:doc ; document
:quality ; rank of completion
:prefix) ; matching state. See also prefixmatches.nim
"Keywords for SexpNode type on nimsuggest.nim.")
(defvar-local nimsuggest--state 'not-started)
(cl-defstruct nim--epc
section symkind qpath file forth line column doc quality prefix)
(defun nimsuggest--parse-epc (epc-result method)
"Parse EPC-RESULT according to METHOD."
(cl-case method
((chk highlight outline) epc-result)
((sug con def use dus)
(cl-mapcar
(lambda (sublist)
(apply #'make-nim--epc
(cl-mapcan #'list nim--epc-keywords sublist)))
epc-result))))
(defvar nimsuggest--epc-processes-alist nil)
(defvar nimsuggest-get-option-function nil
"Function to get options for nimsuggest.")
;; TODO: Is there something needed in this function?
;; https://irclogs.nim-lang.org/12-07-2017.html
(defun nimsuggest-get-options (project-path)
"Get prerequisite options for EPC mode.
PROJECT-PATH is added as the last option."
(delq nil
(append nimsuggest-options nimsuggest-local-options
;; FIXME:
;; In recent nim’s update, this configuration no
;; longer can use.
;; (when (eq 'nimscript-mode major-mode)
;; '("--define:nimscript" "--define:nimconfig"))
(list "--epc" project-path))))
(defun nimsuggest--get-epc-process (file)
"Get active epc process instance for FILE."
(let ((old-epc (cdr (assoc file nimsuggest--epc-processes-alist))))
(if (eq 'run (epc:manager-status-server-process old-epc))
(prog1 old-epc
(nim-log "nimsuggest: use old EPC process\n - %s" old-epc))
(prog1 () (nimsuggest--kill-zombie-processes file)))))
;;;###autoload
(defun nimsuggest-available-p ()
"Return non-nil if nimsuggest is available in current buffer."
(and nimsuggest-path
(not nim--inside-compiler-dir-p)
;; Prevent turn on nimsuggest related feature on org-src block
;; and nimscript-mode (nimsuggest doesn't support yet).
;; https://github.com/nim-lang/nimsuggest/issues/29
(not (memq major-mode '(org-mode nimscript-mode)))
(not (and (fboundp 'org-in-src-block-p)
(or (org-in-src-block-p)
(org-in-src-block-p t))))))
(define-obsolete-function-alias 'nim-suggest-available-p 'nimsuggest-available-p "2017/9/02")
(defun nimsuggest--safe-execute (file func)
"Execute FUNC only if FILE buffer exists."
(save-current-buffer
(let ((buf (get-file-buffer file)))
(when buf
(unless (eq buf (current-buffer)) (set-buffer buf))
(funcall func)))))
(defun nimsuggest--set-state (state file)
"Set STATE for FILE's buffer."
(nimsuggest--safe-execute
file (lambda () (setq-local nimsuggest--state state))))
(defun nimsuggest--start-epc-deferred (file)
"Start EPC process for FILE."
(deferred:nextc (nimsuggest--start-server-deferred nimsuggest-path file)
(lambda (mngr)
(push (cons file mngr) nimsuggest--epc-processes-alist)
(nimsuggest--set-state 'ready file))))
(defun nimsuggest--start-server-deferred (server-prog file)
"Copied from `epc:start-server-deferred' because original function uses `lexicall-let'.
It unable to use from this nim-suggest.el due to some error. (void-variable self or something)
Almost structure is same, but below two values should be changed depending on
nimsuggest's loading time:
- `nimsuggest-accept-process-delay'
- `nimsuggest-accept-process-timeout-count'"
;; TODO: let EPC author knows the issue
(let* ((server-args (nimsuggest-get-options file))
(uid (epc:uid))
(process-name (epc:server-process-name uid))
(process-buffer (get-buffer-create (epc:server-buffer-name uid)))
(process (apply 'start-process
process-name process-buffer
server-prog server-args))
(mngr (make-epc:manager
:server-process process
:commands (cons server-prog server-args)
:title (mapconcat 'identity (cons server-prog server-args) " ")))
(cont 1) port)
(set-process-query-on-exit-flag process nil)
(deferred:$
(deferred:next
;; self recursion during `deferred:lambda' til it gets port
;; number or emits timeout error.
(deferred:lambda (_)
(accept-process-output process 0 nil t)
(let ((port-str (with-current-buffer process-buffer
(buffer-string))))
(cond
((string-match "^[0-9]+$" port-str)
(setq port (string-to-number port-str)
cont nil))
((< 0 (length port-str))
(error "Server may raise an error \
Use \"M-x epc:pop-to-last-server-process-buffer RET\" \
to see full traceback:\n%s" port-str))
((not (eq 'run (process-status process)))
(setq cont nil))
(t
(incf cont)
(when (< nimsuggest-accept-process-timeout-count cont)
(nimsuggest--set-state 'no-response file)
;; timeout 15 seconds (100 * 150)
(error "Timeout server response"))
(deferred:nextc (deferred:wait nimsuggest-accept-process-delay)
self))))))
(deferred:nextc it
(lambda (_)
(setf (epc:manager-port mngr) port)
(setf (epc:manager-connection mngr) (epc:connect "localhost" port))
mngr))
(deferred:nextc it
(lambda (mngr) (epc:init-epc-layer mngr))))))
(defun nimsuggest--query (method callback epc-process)
"Query to nimsuggest of EPC-PROCESS with METHOD.
CALLBACK function will be applied when nimsuggest returns the result."
(when (and (nimsuggest-available-p) epc-process)
;; See also compiler/modulegraphs.nim for dirty file
(let ((temp-dirty-file (nimsuggest--save-buffer-temporarly))
(buf (current-buffer)))
(deferred:$
(epc:call-deferred
epc-process
(prog1 method
(nim-log "EPC-1 %S" (symbol-name method)))
(cl-case method
((chk highlight outline)
(list (buffer-file-name)
-1 -1
temp-dirty-file))
(t
(list (buffer-file-name)
(line-number-at-pos)
(current-column)
temp-dirty-file))))
(deferred:nextc it
(lambda (x)
(nim-log "EPC(%S) nextc" (symbol-name method))
(when x (funcall callback (nimsuggest--parse-epc x method)))))
(deferred:watch it
(lambda (_)
(unless (get-buffer buf)
(nim-log "EPC(%S) delete %s" (symbol-name method) buf)
(delete-file temp-dirty-file))))
(deferred:error it
(lambda (err)
;; Note that it seems like this error clause is not *rare* to
;; called when you write broken nim code. (probably with
;; template or macro) So, just put a log function and see
;; what's going on only if users or I are interested.
(nim-log "EPC(%S) ERROR %s"
(symbol-name method) (error-message-string err))))))))
(defun nimsuggest--call-epc (method callback)
"Call the nimsuggest process on point.
Call the nimsuggest process responsible for the current buffer.
All commands work with the current cursor position. METHOD can be
one of:
sug: suggest a symbol
con: suggest, but called at fun(_ <-
def: where the symbol is defined
use: where the symbol is used
dus: def + use
The CALLBACK is called with a list of ‘nim--epc’ structs."
(let ((file (buffer-file-name)))
(cl-case nimsuggest--state
((never connecting) nil) ; do nothing
(no-response
(nimsuggest--set-state 'never file)
;; Maybe M-x `epc:pop-to-last-server-process-buffer' would be
;; helpful to check the cause.
(message "nimsuggest-mode reached timeout (about %dsec) due to no response from server.
This feature will be blocked on this %s."
(/ (* nimsuggest-accept-process-delay
nimsuggest-accept-process-timeout-count)
1000)
file))
(ready
(nimsuggest--query method callback (nimsuggest--get-epc-process file))
;; Reset `nimsuggest--state' if all epc processes for the file are dead.
;; Not sure if this is related to --refresh option.
(catch 'exit
(cl-loop for (f . _) in nimsuggest--epc-processes-alist
if (equal file f)
do (throw 'exit t)
finally (nimsuggest--set-state 'not-stated file))))
(not-started
(setq-local nimsuggest--state 'connecting)
(deferred:$
(deferred:next
(nimsuggest--start-epc-deferred file))
(deferred:error it
(lambda (err) (nim-log "EPC(startup) ERROR %s" (error-message-string err))))))
(t (error "This shouldn't happen")))))
(defun nimsuggest--call-sync (method callback)
"Synchronous call for nimsuggest using METHOD.
The CALLBACK function is called when it got the response."
(let* ((buf (current-buffer))
(start (time-to-seconds))
(res 'trash))
(nimsuggest--call-epc
method
(lambda (candidates)
(when (eq (current-buffer) buf)
(setq res (funcall callback candidates)))))
(while (and (eq 'trash res) (eq (current-buffer) buf))
(if (> (- (time-to-seconds) start) 2)
(nim-log "EPC-sync(%s): timeout %d sec" (symbol-name method) 2)
(sleep-for 0.03)))
(unless (eq 'trash res)
res)))
(defun nimsuggest--get-dirty-dir ()
"Return temp directory.
The directory name consists of `nimsuggest-dirty-directory' and current
frame number. The frame number is required to prevent Emacs
crash when some emacsclients open the same file."
(let* ((frame-num (car (last (split-string (format "%s" (selected-frame)) " "))))
(frame-num-str (substring frame-num 0 (1- (length frame-num)))))
(file-name-as-directory (concat nimsuggest-dirty-directory frame-num-str))))
(defun nimsuggest--get-temp-file-name ()
"Get temp file name."
(mapconcat 'directory-file-name
`(,(nimsuggest--get-dirty-dir)
,(cl-case system-type
((ms-dos windows-nt cygwin)
;; For bug #119, convert ":" to "꞉" (U+A789)
(concat "/"
(replace-regexp-in-string
":" (char-to-string #xA789)
buffer-file-name)))
(t ; for *nix system
buffer-file-name)))
""))
(defun nimsuggest--make-tempdir (tempfile)
"Make temporary directory for TEMPFILE."
(let* ((tempdir (file-name-directory tempfile)))
(unless (file-exists-p tempdir)
(make-directory tempdir t))))
(defun nimsuggest--save-buffer-temporarly ()
"Save the current buffer and return the location."
(let* ((temporary-file-directory nimsuggest-dirty-directory)
(filename (nimsuggest--get-temp-file-name)))
(nimsuggest--make-tempdir filename)
(save-restriction
(widen)
(write-region (point-min) (point-max) filename nil 1))
filename))
(add-hook 'kill-emacs-hook 'nimsugget--delete-temp-directory)
(defun nimsugget--delete-temp-directory ()
"Delete temporary files directory for nimsuggest."
(when (file-exists-p nimsuggest-dirty-directory)
(delete-directory (file-name-directory nimsuggest-dirty-directory) t)))
(defun nimsuggest--kill-zombie-processes (&optional ppath)
"Kill needless zombie processes, which correspond to PPATH."
(setq nimsuggest--epc-processes-alist
(cl-loop for (file . manager) in nimsuggest--epc-processes-alist
if (and (epc:live-p manager)
(or (and ppath (equal ppath file))
(not ppath)))
collect (cons file manager)
else do (epc:stop-epc manager))))
(defvar nimsuggest-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-d") #'nimsuggest-show-doc)
map))
(defcustom nimsuggest-mode-hook nil
"Hook run when entering Nimsuggest mode."
:options '(flycheck-nimsuggest-setup nimsuggest-flymake-setup nimsuggest-xref-setup)
:type 'hook
:group 'nim)
;;;###autoload
(define-minor-mode nimsuggest-mode
"Minor mode for nimsuggest."
:lighter " nimsuggest"
:keymap nimsuggest-mode-map
(when nimsuggest-mode
(nimsuggest-ensure)))
(defun nimsuggest-force-stop ()
"Try to stop nimsuggest related things, but not well tested."
(interactive)
(remove-hook 'flycheck-checkers 'nim-nimsuggest)
(remove-hook 'flymake-diagnostic-functions 'flymake-nimsuggest t)
(nim-eldoc-off)
(nimsuggest-xref-on-or-off 'off))
(defun nimsuggest-ensure ()
"Ensure that users installed nimsuggest executable."
;; I've seen so many people just stacked to install nimsuggest at
;; first time. Probably this package's name is kinda confusing.
(interactive)
(let ((msg "Nimsuggest-mode needs external tool called nimsuggest.
Generally you can build by './koch tools' or '.koch nimsuggest'
on Nim repo (check koch.nim file), but it's good to check README
on Nim's official repository on yourself in case this document
was outdated."))
(when (or (bound-and-true-p eldoc-mode)
(bound-and-true-p global-eldoc-mode))
(setq nimsuggest-eldoc-function 'nimsuggest-eldoc--nimsuggest))
(when (not nimsuggest-path)
(nimsuggest-force-stop)
(error msg))
(when (not (file-executable-p nimsuggest-path))
(nimsuggest-force-stop)
(error "`nimsuggest-path' isn't executable; %s" msg))
(if nimsuggest-mode
(nim-log "nimsuggest-mode started")
(nim-log "nimsuggest-mode stopped"))))
;; Utilities
(defun nimsuggest--put-face (text face)
"Put FACE on the TEXT."
(when (and text (string< "" text))
(add-text-properties
0 (length text)
`(face ,face)
text)))
(defun nimsuggest--parse (forth)
"Parse FORTH element."
(when (string-match
(rx (group (1+ word)) (0+ " ")
(group (1+ nonl)))
forth)
(let ((first (match-string 1 forth))
(other (match-string 2 forth)))
(cons first other))))
(defun nimsuggest--trim (str)
"Adjust STR for mini buffer."
(let ((max-width (- (frame-width) 4))) ; <- just for buffer, probably
(if (< (length str) max-width) ; it depends on terminal or GUI Emacs
str
(let* ((short-str (substring str 0 (- (frame-width) 4)))
(minus-offset
(cl-loop with num = 0
for s in (delq "" (split-string (reverse short-str) ""))
if (equal s ".") do (cl-return num)
else do (cl-incf num)
finally return 0)))
(substring short-str 0 (- (length short-str) minus-offset))))))
(defun nimsuggest--format (forth symKind qpath doc)
"Highlight returned result from nimsuggest of FORTH, SYMKIND, QPATH, and DOC."
(let* ((doc (mapconcat 'identity (split-string doc "\n") ""))
(name
(if (eq (length (cdr qpath)) 1)
(cadr qpath)
(mapconcat 'identity (cdr qpath) "."))))
(nimsuggest--put-face doc font-lock-doc-face)
(pcase (list symKind)
(`(,(or "skProc" "skField" "skTemplate" "skMacro"))
(when (string< "" forth)
(cl-destructuring-bind (ptype . typeinfo) (nimsuggest--parse forth)
(when (equal "proc" ptype)
(nimsuggest--put-face name font-lock-function-name-face)
(let* ((func (format "%s %s" name typeinfo)))
(nimsuggest--trim
(if (string= "" doc)
(format "%s" func)
(format "%s %s" func doc))))))))
(`(,(or "skVar" "skLet" "skConst" "skResult" "skParam"))
(let ((sym (downcase (substring symKind 2 (length symKind)))))
(nimsuggest--put-face sym font-lock-keyword-face)
(nimsuggest--put-face name
(cond ((member symKind '("skVar" "skResult"))
'(face font-lock-variable-name-face))
((member symKind '("skLet" "skConst"))
'(face font-lock-constant-face))
(t '(face font-lock-keyword-face))))
(nimsuggest--trim
(format "%s %s : %s" sym name
(cond
((string< "" forth) forth)
(t "no doc"))))))
(`("skType")
(nimsuggest--put-face name font-lock-type-face)
(nimsuggest--trim
(if (not (string< "" doc))
(format "%s: no doc" name)
(format "%s: %s" name doc)))))))
;;; misc
;; work in progress
(defcustom nimsuggest-doc-directive
'def
"Directive passed by nimsuggest for `nimsuggest-show-doc'."
:type '(choice
(const :tag "suggest" 'sug)
(const :tag "definition" 'def))
:group 'nim)
(defvar nimsuggest--doc-args nil
"Internal variable to store document data.")
(defun nimsuggest-show-doc ()
"Show document in dedicated *nim-doc* buffer."
(interactive)
(nimsuggest--call-epc
nimsuggest-doc-directive
(lambda (args)
(if (and (not args) (not (eq 'sug nimsuggest-doc-directive)))
;; Fallback if there is no result from nimsuggest by 'sug
(let ((nimsuggest-doc-directive 'sug))
(nimsuggest-show-doc))
;; TODO: should I filter returned result by current position's identifier?
(setq nimsuggest--doc-args (cl-loop for i from 0 to (1- (length args))
collect (cons (1+ i) (nth i args))))
(nimsuggest--show-doc)))))
(defun nimsuggest--show-doc ()
"Internal function for `nimsuggest-show-doc'."
(let ((def (cdar nimsuggest--doc-args)))
(get-buffer-create "*nim-doc*")
(unless (equal (current-buffer) (get-buffer "*nim-doc*"))
(switch-to-buffer-other-window "*nim-doc*"))
(setq buffer-read-only nil)
(erase-buffer)
(cl-mapcar
(lambda (x) (insert (concat x "\n")))
(list
;; (format "debug %s\n" nimsuggest--doc-args)
(let ((nominator (caar nimsuggest--doc-args))
(denominator (length nimsuggest--doc-args)))
(format "%s %s\n"
(mapconcat 'identity (nim--epc-qpath def) " ")
(if (eq 1 denominator)
""
(format "%s/%s %s" nominator denominator
"-- < next, > previous"))))
(format "Signature\n#########\n%s\n"
(format "%s %s"
(nim--epc-symkind def)
(nim--epc-forth def)))
(format "Document\n########\n%s\n"
(nim--epc-doc def))
(format "Location\n########\n%s\n"
(nim--epc-file def))))
;; For highlight stuff
(when (fboundp 'rst-mode) (rst-mode))
(goto-char (point-min))
(use-local-map nimsuggest-doc-mode-map)
(setq buffer-read-only t)))
(defun nimsuggest-doc-next ()
"Move to next page."
(interactive)
(if (not (< 0 (length nimsuggest--doc-args)))
(minibuffer-message "there is no next")
(let ((popped (pop nimsuggest--doc-args)))
(setq nimsuggest--doc-args (append nimsuggest--doc-args (list popped)))
(nimsuggest--show-doc))))
(defun nimsuggest-doc-previous ()
"Move to previous page."
(interactive)
(if (not (< 0 (length nimsuggest--doc-args)))
(minibuffer-message "there is no previous")
(let* ((rargs (reverse nimsuggest--doc-args))
(popped (pop rargs)))
(setq rargs (append rargs (list popped))
nimsuggest--doc-args (reverse rargs))
(nimsuggest--show-doc))))
;;; Flymake integration
;; From Emacs 26, flymake was re-written by João Távora.
;; It supports asynchronous backend, so enable it if users
;; turned on the flymake-mode.
;; Manual configuration:
;; (add-hook 'nimsuggest-mode-hook 'nimsuggest-flymake-setup)
;; TODO: specify more specific version
(when (version<= "26" (number-to-string emacs-major-version))
(add-hook 'nimsuggest-mode-hook 'nimsuggest-flymake-setup))
;;;###autoload
(defun nimsuggest-flymake-setup()
"Kinda experimental function to use flymake on Emacs 26."
(when (and (bound-and-true-p flymake-mode)
(not (bound-and-true-p flycheck-mode)))
(if nimsuggest-mode
(add-hook 'flymake-diagnostic-functions 'flymake-nimsuggest nil t)
(remove-hook 'flymake-diagnostic-functions 'flymake-nimsuggest t))))
(defun nimsuggest--flymake-filter (errors buffer)
"Remove not related ERRORS in the BUFFER."
(cl-loop for (_ _ _ file type line col text _) in errors
if (and (eq buffer (get-file-buffer file))
(<= 1 line) (<= 0 col))
;; column needs to be increased by 1 to highlight correctly
collect (list file type line (1+ col) text)))
(defun nimsuggest--flymake-region (file buf line col)
"Calculate beg and end for FILE, BUF, LINE, and COL.
Workaround for https://github.com/nim-lang/nim-mode/issues/183."
(if (not (eq (get-file-buffer file) (current-buffer)))
(cons 0 1)
(cl-letf* (((symbol-function 'end-of-thing)
(lambda (&rest _r) nil)))
(funcall 'flymake-diag-region buf line col))))
(defun nimsuggest--flymake-error-parser (errors buffer)
"Return list of result of `flymake-make-diagnostic' from ERRORS.
The list can be nil. ERRORS will be skipped if BUFFER and
parsed file was different."
(cl-loop with errs = (nimsuggest--flymake-filter errors buffer)
for (file typ line col text) in errs
for type = (cl-case (string-to-char typ)
(?E :error)
(?W :warning)
(t :note))
for (beg . end) = (nimsuggest--flymake-region file buffer line col)
collect (funcall 'flymake-make-diagnostic buffer beg end type text)))
(defun flymake-nimsuggest (report-fn &rest _args)
"A Flymake backend for Nim language using Nimsuggest.
See `flymake-diagnostic-functions' for REPORT-FN and ARGS."
(let ((buffer (current-buffer)))
(nimsuggest--call-epc
'chk
(lambda (errors)
(nim-log "FLYMAKE(start): report(s) number of %i" (length errors))
(condition-case err
(let ((report-action
(nimsuggest--flymake-error-parser errors buffer)))
(funcall report-fn report-action))
(error
(nim-log "FLYMAKE(error): %s" (error-message-string err))))))))
;; TODO: is this really needed?
(defun nimsuggest-flymake--panic (report-fn err)
"TODO: not sure where to use this yet...
Using this function cause to stop flymake completely which is not
suitable for nimsuggest because nimsuggest re-start after its
crush.
You can find explanation REPORT-FN at `flymake-diagnostic-functions'
and the ERR is captured error."
(when (member 'flymake-nimsuggest flymake-diagnostic-functions)
(nim-log-err "FLYMAKE(ERR): %s" err)
(funcall report-fn :panic :explanation err)))
;;; ElDoc for nimsuggest
(defvar nimsuggest-eldoc--data nil)
;;;###autoload
(defun nimsuggest-eldoc--nimsuggest ()
"Update `eldoc-last-message' by nimsuggest's information."
(when (nimsuggest-available-p)
(unless (nimsuggest-eldoc--same-try-p)
(nimsuggest-eldoc--call))
(when (eq (line-number-at-pos)
(assoc-default :line nimsuggest-eldoc--data))
(assoc-default :str nimsuggest-eldoc--data))))
(defun nimsuggest-eldoc--same-try-p ()
"Predicate function if same try or not."
(or (and (equal (nim-current-symbol)
(assoc-default :name nimsuggest-eldoc--data))
(eq (assoc-default :line nimsuggest-eldoc--data)
(line-number-at-pos)))
(and (nim-eldoc-inside-paren-p)
(save-excursion
(nimsuggest-eldoc--move)
(or
;; for template
(eq (point) (assoc-default :pos nimsuggest-eldoc--data))
;; for proc
(eq (1- (point)) (assoc-default :pos nimsuggest-eldoc--data)))))))
(defun nimsuggest-eldoc--move ()
"Move cursor appropriate point where calling nimsuggest is suitable."
(let ((pos (point))
(ppss (syntax-ppss)))
(when (nim-eldoc-inside-paren-p)
(save-excursion
(goto-char (nth 1 ppss))
(when (looking-back nim-eldoc--skip-regex nil)
(goto-char pos))))))
(defun nim-eldoc-format-string (defs)
"Format data inside DEFS for eldoc.
DEFS is group of definitions from nimsuggest."
;; TODO: switch if there are multiple defs
(nim-log "ELDOC format")
(let ((data (cl-first defs)))
(apply 'nimsuggest--format
(mapcar (lambda (x) (funcall x data))
'(nim--epc-forth nim--epc-symkind nim--epc-qpath nim--epc-doc)))))
(defun nimsuggest-eldoc--call ()
"Call nimsuggest for eldoc."
(save-excursion
(nimsuggest-eldoc--move)
(nim-log "ELDOC-1")
(nimsuggest--call-epc 'dus 'nimsuggest-eldoc--update)))
(defun nimsuggest-eldoc--update (def-use)
"Update eldoc information from DEF-USE of nimsuggest."
(if (not (nim-eldoc--try-p))
(nim-log "ELDOC stop update")
(nim-log "ELDOC update")
(if def-use
(nimsuggest-eldoc--update-1 def-use)
(save-excursion
(when (nim-eldoc-inside-paren-p)
(nimsuggest-eldoc--move)
(backward-char)
(nimsuggest--call-epc 'dus 'nimsuggest-eldoc--update-1))))))
(defun nimsuggest-eldoc--update-1 (epc-result)
"Save EPC-RESULT into `nimsuggest-eldoc--data'.
And show message of `eldoc-last-message'.
The EPC-RESULT can be result of both def and/or dus."
(when epc-result
(setq nimsuggest-eldoc--data
(list
(cons :str (nim-eldoc-format-string epc-result))
(cons :line (line-number-at-pos))
(cons :name (nim-current-symbol))
(cons :pos (point))))
(setq eldoc-last-message (assoc-default :str nimsuggest-eldoc--data))
(message eldoc-last-message)))
;;; xref integration
;; This package likely be supported on Emacs 25.1 or later
(defvar nimsuggest-find-definition-function nil
"Function for `nimsuggest-find-definition'.")
;;;###autoload (add-hook 'nimsuggest-mode-hook 'nimsuggest-xref-setup)
;;;###autoload
(defun nimsuggest-xref-setup ()
"Setup xref backend for nimsuggest."
(cond
((not (nimsuggest-available-p))
(nim-log "xref package needs nimsuggest"))
((not (require 'xref nil t))
(setq nimsuggest-find-definition-function 'nimsuggest-find-definition-old)
;; Note below configuration were removed on the future
(define-key nimsuggest-mode-map (kbd "M-.") #'nimsuggest-find-definition)
(define-key nimsuggest-mode-map (kbd "M-,") #'pop-tag-mark))
((version<= "25.1.0" emacs-version)
(require 'xref)
(setq nimsuggest-find-definition-function 'xref-find-definitions)
(nimsuggest-xref-on-or-off (if nimsuggest-mode 'on 'off)))
(t (nim-log "xref unexpected condition"))))
(defun nimsuggest-xref-on-or-off (on-or-off)
"Turn on or off xref feature for nimsuggest backend.
You can specify `on' or `off' symbol as the ON-OR-OFF."
(cl-case on-or-off
(on (add-hook 'xref-backend-functions #'nimsuggest--xref-backend nil t))
(off (remove-hook 'xref-backend-functions #'nimsuggest--xref-backend t))))
(defun nimsuggest-find-definition ()
"This function is preserved for backward compatibility.
If your Emacs support cross reference library `xref' (from Emacs
25.1), you might want to use `xref-find-definition' instead which
binds to `M-.' in default."
(interactive)
(call-interactively nimsuggest-find-definition-function))
;; Define xref backend for nimsuggest
(with-eval-after-load "xref"
(defun nimsuggest--xref-backend () 'nimsuggest)
(cl-defmethod xref-backend-identifier-at-point ((_backend (eql nimsuggest)))
"Return string or nil for identifier at point."
;; Well this function may not needed for current xref functions for
;; nimsuggest backend.
(with-syntax-table nim-dotty-syntax-table
(let ((thing (thing-at-point 'symbol)))
(and thing (substring-no-properties thing)))))
(defun nimsuggest--xref-make-obj (id def)
(let ((summary id)
(location (xref-make-file-location
(nim--epc-file def)
(nim--epc-line def)
(nim--epc-column def))))
(xref-make summary location)))
(defun nimsuggest--xref (query id)
(nimsuggest--call-sync
query
(lambda (results)
(cond
((null results) nil)
((listp results)
(cl-loop for result in results
collect (nimsuggest--xref-make-obj id result)))))))
(cl-defmethod xref-backend-definitions ((_backend (eql nimsuggest)) id)
(nimsuggest--xref 'def id))
(cl-defmethod xref-backend-references ((_backend (eql nimsuggest)) id)
(nimsuggest--xref 'dus id))
;; just define empty backend to use `xref-backend-references' for
;; nimsuggest.
(cl-defmethod xref-backend-identifier-completion-table
((_backend (eql nimsuggest))))
;; Not implement yet, or not sure maybe, won't...
;; (cl-defmethod xref-backend-apropos ((_backend (eql nimsuggest)) pattern))
) ; end of with-eval-after-load xref
;; Workaround for old Emacsen
;; TODO: remove those stuff after Emacs 25 or 26 is dominant.
(require 'etags)
(defun nimsuggest-find-definition-old ()
"Go to the definition of the symbol currently under the cursor."
(nimsuggest--call-epc
'def
(lambda (defs)
(let ((def (cl-first defs)))
(when (not def) (error "Definition not found"))
(if (fboundp 'xref-push-marker-stack)
(xref-push-marker-stack)
(with-no-warnings
(ring-insert find-tag-marker-ring (point-marker))))
(find-file (nim--epc-file def))
(goto-char (point-min))
(forward-line (1- (nim--epc-line def)))))))
(define-obsolete-function-alias 'nim-goto-sym 'nimsuggest-find-definition
"2017/9/02")
;;; Debug
(defun nimsuggest--put (text where)
"Put text property to TEXT of WHERE."
(put-text-property where (1+ where) 'face 'success text))
(defun nimsuggest--debug-prompt ()
"Return string for read key."
(let ((msgs '(sug (def . 2) dus use con highlight outline)))
(cl-loop for msg in msgs
for text = (symbol-name (if (symbolp msg) msg (car msg)))
for where = (if (symbolp msg) 0 (cdr msg))
do (nimsuggest--put text where)
collect text into res
finally return (mapconcat 'identity res " "))))
(defun nimsuggest--debug-print ()
"Print result of nimsuggest's epc call."
(interactive)
(let ((input (read-key (nimsuggest--debug-prompt))))
(let ((key (cl-case input
(?s 'sug)
(?f 'def)
(?d 'dus)
(?u 'use)
(?c 'con)
(?h 'highlight)
(?o 'outline))))
(if (null key)
(minibuffer-message "unexpected key %c" input)
(message "calling nimsuggest ...")
(nimsuggest--call-epc
key
(lambda (args)
(message "nimsuggest's result:\n%s" args)))))))
;; (define-key nimsuggest-mode-map (kbd "C-8") 'nimsuggest--debug-print)
(provide 'nim-suggest)
;;; nim-suggest.el ends here