summaryrefslogtreecommitdiff
path: root/elpa/irony-20220110.849/irony.el
blob: f70eca3a3d429f85bab4ac88d0aa878249b2f493 (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
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
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
;;; irony.el --- C/C++ minor mode powered by libclang

;; Copyright (C) 2011-2016  Guillaume Papin

;; Author: Guillaume Papin <guillaume.papin@epitech.eu>
;; Version: 1.5.0
;; URL: https://github.com/Sarcasm/irony-mode
;; Compatibility: GNU Emacs 24.x
;; Keywords: c, convenience, tools
;; Package-Requires: ((cl-lib "0.5") (json "1.2"))

;; 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:
;;
;; This file provides `irony-mode', a minor mode for C, C++ and Objective-C.
;;
;; Usage:
;;     (add-hook 'c++-mode-hook 'irony-mode)
;;     (add-hook 'c-mode-hook 'irony-mode)
;;     (add-hook 'objc-mode-hook 'irony-mode)
;;
;;     ;; Windows performance tweaks
;;     ;;
;;     (when (boundp 'w32-pipe-read-delay)
;;       (setq w32-pipe-read-delay 0))
;;     ;; Set the buffer size to 64K on Windows (from the original 4K)
;;     (when (boundp 'w32-pipe-buffer-size)
;;       (setq irony-server-w32-pipe-buffer-size (* 64 1024)))
;;
;; See also:
;; - https://github.com/Sarcasm/company-irony
;; - https://github.com/Sarcasm/flycheck-irony
;; - https://github.com/Sarcasm/ac-irony

;;; Code:

(require 'irony-iotask)

(autoload 'irony-completion--enter "irony-completion")
(autoload 'irony-completion--exit "irony-completion")

(require 'cl-lib)

(autoload 'find-library-name "find-func")
(autoload 'lm-version "lisp-mnt")


;;
;; Compatibility
;;

(eval-and-compile

  ;; As seen in flycheck/magit
  ;;
  ;; Added in Emacs 24.3 (mirrors/emacs@b335efc3).
  (unless (fboundp 'setq-local)
    (defmacro setq-local (var val)
      "Set variable VAR to value VAL in current buffer."
      (list 'set (list 'make-local-variable (list 'quote var)) val)))

  ;; Added in Emacs 24.3 (mirrors/emacs@b335efc3).
  (unless (fboundp 'defvar-local)
    (defmacro defvar-local (var val &optional docstring)
      "Define VAR as a buffer-local variable with default value VAL.
Like `defvar' but additionally marks the variable as being
automatically buffer-local wherever it is set."
      (declare (debug defvar) (doc-string 3))
      (list 'progn (list 'defvar var val docstring)
            (list 'make-variable-buffer-local (list 'quote var)))))

  ) ;; eval-and-compile


;;
;; Customizable variables
;;

(defgroup irony nil
  "C/C++ minor mode powered by libclang."
  :group 'c)

(defcustom irony-lighter " Irony"
  "Text to display in the mode line when irony mode is on."
  :type 'string
  :group 'irony)

(defcustom irony-extra-cmake-args nil
  "Extra arguments to CMake when compiling the server."
  :type '(repeat string)
  :group 'irony)

(defcustom irony-user-dir (locate-user-emacs-file "irony/")
  "Directory containing the Irony generated files.

The slash is expected at the end."
  :type 'directory
  :risky t
  :group 'irony)

(defcustom irony-supported-major-modes '(c++-mode
                                         c-mode
                                         objc-mode)
  "List of modes known to be compatible with Irony."
  :type '(repeat symbol)
  :group 'irony)

;;;###autoload
(defcustom irony-additional-clang-options nil
  "Additional command line options to pass down to libclang.

Please, do NOT use this variable to add header search paths, only
additional warnings or compiler options.

These compiler options will be prepended to the command line, in
order to not override the value coming from a compilation
database."
  :type '(repeat string)
  :options '("-Wdocumentation")
  :group 'irony)

(defcustom irony-lang-compile-option-alist
  '((c++-mode  . "c++")
    (c-mode    . "c")
    (objc-mode . "objective-c"))
  "Alist to decide the language option to used based on the `major-mode'."
  :type '(alist :key-type symbol :value-type string)
  :group 'irony)

(defcustom irony-cmake-executable "cmake"
  "Name or path of the CMake executable."
  :type 'string
  :group 'irony)

(defcustom irony-server-source-dir nil
  "Points to the irony-server source directory.

This should point to the directory that contains the top-most
CMakeLists.txt used to build the server.

By default it will find the directory based on the irony.el directory."
  :type 'directory
  :group 'irony
  :package-version '(irony . "1.2.0"))

(defcustom irony-server-build-dir nil
  "Build directory for irony-server.

If set to nil the default is to create a build directory in
`temporary-file-directory'/build-irony-server-`(irony-version)'."
  :type 'directory
  :group 'irony)

(defcustom irony-server-install-prefix irony-user-dir
  "Installation prefix used to install irony-server.

The irony-server executable is expected to be in
`irony-server-install-prefix'/bin/."
  :type 'directory
  :group 'irony)

(defcustom irony-server-w32-pipe-buffer-size nil
  "Windows-only setting,
the buffer size to use for the irony-server process pipe on Windows.

Larger values can improve performances on large buffers.

If non-nil, `w32-pipe-buffer-size' will be let-bound to this value
during the creation of the irony-server process.")


;;
;; Public/API variables
;;
;; Non-customizable variables provided by Irony that can be useful to other
;; packages.
;;
;; Note that they shouldn't be modified directly by external packages, just
;; read.
;;

;; TODO: make this variable public when the CDB API stabilizes.
(defvar-local irony--compile-options nil
  "Compile options for the current file.

The compile options used by the compiler to build the current
buffer file.")

;; TODO: make this variable public when the CDB API stabilizes.
(defvar-local irony--working-directory nil
  "The working directory to pass to libclang, if any.")


;;
;; Internal variables
;;
;; The prefix `irony--' is used when something can completely change (or
;; disappear) from one release to the other.
;;
;; -- https://lists.gnu.org/archive/html/emacs-devel/2013-06/msg01129.html

(defconst irony--eot "\n;;EOT\n"
  "String sent by the server to signal the end of a response.")


;;
;; Error conditions
;;

;; `define-error' breaks backward compatibility with Emacs < 24.4
(defun irony--define-error (name message &optional parent)
  "Define NAME as a new error signal.
MESSAGE is a string that will be output to the echo area if such an error
is signaled without being caught by a `condition-case'.
PARENT is either a signal or a list of signals from which it inherits.
Defaults to `error'."
  (unless parent (setq parent 'error))
  (let ((conditions
         (if (consp parent)
             (apply #'nconc
                    (mapcar (lambda (parent)
                              (cons parent
                                    (or (get parent 'error-conditions)
                                        (error "Unknown signal `%s'" parent))))
                            parent))
           (cons parent (get parent 'error-conditions)))))
    (put name 'error-conditions
         (delete-dups (copy-sequence (cons name conditions))))
    (when message (put name 'error-message message))))

(irony--define-error 'irony-error "Irony-Mode error")
(irony--define-error 'irony-parse-error "Irony-Mode parsing error" 'irony-error)
(irony--define-error 'irony-server-error "Irony-Mode server error" 'irony-error)


;;
;; Utility functions & macros
;;

;; TODO: remove and use `if-let' when supported version jumps to Emacs 25.1
(defmacro irony--aif (test if-expr &rest else-body)
  (declare (indent 2))
  `(let ((it ,test))
     (if it
         ,if-expr
       (progn ,@else-body))))

;; TODO: remove and use `when-let' when supported version jumps to Emacs 25.1
(defmacro irony--awhen (test &rest body)
  (declare (indent 1))
  `(let ((it ,test))
     (when it
       (progn ,@body))))

(defun irony--assoc-all (key list)
  (delq nil (mapcar (lambda (c)
                      (when (equal (car c) key)
                        c))
                    list)))

(defmacro irony--without-narrowing (&rest body)
  "Remove the effect of narrowing for the current buffer.

Note: If `save-excursion' is needed for BODY, it should be used
before calling this macro."
  (declare (indent 0) (debug t))
  `(save-restriction
     (widen)
     (progn ,@body)))

(defun irony--buffer-size-in-bytes ()
  "Return the buffer size, in bytes."
  (1- (position-bytes (point-max))))

(defun irony--read-char-choice (prompt chars)
  "Wrapper around `read-char-choice', available since Emacs 24."
  (setq prompt (concat prompt " [" chars "]: "))
  (if (fboundp 'read-char-choice)
      (read-char-choice prompt chars)
    (setq prompt (propertize prompt 'face 'minibuffer-prompt))
    (let ((cursor-in-echo-area t)
          k)
      (while (not (member k chars))
        (setq k (read-char-exclusive prompt)))
      k)))

(defun irony--shorten-path (path)
  "Make PATH as short as possible.

The given path can be considered understandable by human but not
necessary a valid path string to use in code. Its only purpose is
to be displayed to the user."
  (let ((relative (file-relative-name path))
        (abbreviated (abbreviate-file-name path)))
    (if (< (string-width relative) (string-width abbreviated))
        relative
      abbreviated)))

(defun irony--split-command-line-1 (quoted-str)
  "Remove the escaped quotes and backlash from a QUOTED-STR.

Return a list of the final characters in the reverse order.

Only to be consumed by `irony--split-command-line'."
  (let ((len (length quoted-str))
        (i 0)
        ch next-ch
        result)
    (while (< i len)
      (setq ch (aref quoted-str i))
      (when (eq ch ?\\)
        (let ((next-ch (and (< (1+ i) len)
                            (aref quoted-str (1+ i)))))
          (when (member next-ch '(?\\ ?\"))
            (setq ch next-ch)
            (cl-incf i))))
      (push ch result)
      (cl-incf i))
    result))

;; TODO: rewrite the function correctly to handle things like the following:
;;
;; "/usr/bin/clang++ -Irelative -DSOMEDEF=\"With spaces, quotes and \\-es.\" <args...>"
(defun irony--split-command-line (cmd-line)
  "Split CMD-LINE into a list of arguments.

Takes care of double quotes as well as backslash.

Sadly I had to write this because `split-string-and-unquote'
breaks with escaped quotes in compile_commands.json, such as in:

    /usr/bin/c++ -DLLVM_VERSION_INFO=\\\\\\\"3.2svn\\\\\\\" <args>"
  ;; everytime I write a function like this one, it makes me feel bad
  (let* ((len (length cmd-line))
         (spaces (string-to-list " \f\t\n\r\v"))
         (first-not-spaces-re (concat "[^" spaces "]"))
         (i 0)
         ch
         args cur-arg)
    (while (< i len)
      (setq ch (aref cmd-line i))
      (cond
       ((member ch spaces)              ;spaces
        (when cur-arg
          (setq args (cons (apply 'string (nreverse cur-arg)) args)
                cur-arg nil))
        ;; move to the next char
        (setq i (or (string-match-p first-not-spaces-re cmd-line i)
                    len)))
       ((eq ch ?\")                     ;quoted string
        (let ((endq (string-match-p "[^\\]\"" cmd-line i)))
          (unless endq
            (signal 'irony-parse-error (list "ill formed command line" cmd-line)))
          (let ((quoted-str (substring cmd-line (1+ i) (1+ endq))))
            (setq cur-arg (append (irony--split-command-line-1 quoted-str)
                                  cur-arg)
                  i (+ endq 2)))))
       (t                             ;a valid char
        ;; if it's an escape of: a backslash, a quote or a space push
        ;; only the following char.
        (when (eq ch ?\\)
          (let ((next-ch (and (< (1+ i) len)
                              (aref cmd-line (1+ i)))))
            (when (or (member next-ch '(?\\ ?\"))
                      (member next-ch spaces))
              (setq ch next-ch)
              (cl-incf i))))
        (push ch cur-arg)
        (cl-incf i))))
    (when cur-arg
      (setq args (cons (apply 'string (nreverse cur-arg)) args)))
    (nreverse args)))

(defun irony--get-buffer-path-for-server (&optional buffer)
  "Get the path of the current buffer to send to irony-server.

If no such file exists on the filesystem the special file '-' is
  returned instead."
  (let ((file (buffer-file-name buffer)))
    (if (and file (file-exists-p file))
        file
      "-")))


;;
;; Mode
;;

(defvar irony-mode-map (make-sparse-keymap)
  "Keymap used in `irony-mode' buffers.")

;;;###autoload
(define-minor-mode irony-mode
  "Minor mode for C, C++ and Objective-C, powered by libclang."
  nil
  irony-lighter
  irony-mode-map
  :group 'irony
  (if irony-mode
      (irony--mode-enter)
    (irony--mode-exit)))

(defun irony--mode-enter ()
  ;; warn the user about modes such as php-mode who inherits c-mode
  (when (not (memq major-mode irony-supported-major-modes))
    (display-warning 'irony "Major mode is unknown to Irony,\
 see `irony-supported-major-modes'."))
  ;; warn the user about Windows-specific issues
  (when (eq system-type 'windows-nt)
    (cond
     ((version< emacs-version "24.4")
      (display-warning 'irony "Emacs >= 24.4 expected on Windows."))
     ((and (boundp 'w32-pipe-read-delay) (> w32-pipe-read-delay 0))
      (display-warning 'irony "Performance will be bad because a\
 pipe delay is set for this platform (see variable\
 `w32-pipe-read-delay')."))))
  (irony-completion--enter))

(defun irony--mode-exit ()
  (irony-completion--exit))

;;;###autoload
(defun irony-version (&optional show-version)
  "Return the version number of the file irony.el.

If called interactively display the version in the echo area."
  (interactive (list t))
  ;; Shamelessly stolen from `company-mode'.
  (with-temp-buffer
    (insert-file-contents (find-library-name "irony"))
    (let ((v (lm-version)))
      (when show-version
        (message "irony version: %s" v))
      v)))


;;
;; Compile options handling
;;

(defun irony--lang-compile-option ()
  (irony--awhen (cdr-safe (assq major-mode irony-lang-compile-option-alist))
    (list "-x" it)))

(defun irony--extract-working-directory-option (flags)
  "Return working directory specified on the command line, if
any."
  (catch 'found
    (while flags
      (let ((flag (car flags)))
        (cond
         ((string= "-working-directory" flag)
          (throw 'found (cadr flags)))
         ((string-prefix-p "-working-directory=" flag)
          (throw 'found (substring flag (length "-working-directory="))))
         (t
          (setq flags (cdr flags))))))))

(defun irony--adjust-compile-options ()
  "The compile options to send to libclang."
  ;; TODO: if current buffer has no associated file (will be sent as '-') but is
  ;; in an existing directory, we will want to add -I (directory-file-name
  ;; buffer-file-name) to find the relative headers
  (append
   (irony--lang-compile-option)
   (irony--awhen irony--working-directory
     (unless (irony--extract-working-directory-option irony--compile-options)
       (list "-working-directory" it)))
   irony-additional-clang-options
   irony--compile-options))

(defun irony--extract-user-search-paths (compile-options work-dir)
  "Retrieve the user search paths present in COMPILE-OPTIONS.

Relative paths are expanded to be relative to WORK-DIR.

The returned paths are returned as
directory (`file-name-as-directory').

Note: WORK-DIR is not used when the compile option
'-working-directory=<directory>' is detected in COMPILE-OPTIONS."
  (setq work-dir (or (irony--extract-working-directory-option compile-options)
                     work-dir))
  (let (include-dirs opt)
    (while (setq opt (car compile-options))
      (cond
       ((string= "-I" opt)
        (add-to-list 'include-dirs (nth 1 compile-options) t)
        (setq compile-options (cddr compile-options)))
       ((string-prefix-p "-I" opt)
        (add-to-list 'include-dirs (substring opt 2) t)
        (setq compile-options (cdr compile-options)))
       (t
        (setq compile-options (cdr compile-options)))))
    (delete-dups (mapcar #'(lambda (path)
                             (file-name-as-directory
                              (expand-file-name path work-dir)))
                         include-dirs))))


;;
;; Irony-Server setup
;;

(defvar irony--server-install-command-history nil)
(defun irony--install-server-read-command (command)
  (read-shell-command
   "Install command: " command
   (if (equal (car irony--server-install-command-history) command)
       '(irony--server-install-command-history . 1)
     'irony--server-install-command-history)))

(defun irony-install-server (command)
  "Install or reinstall the Irony server.

The installation requires CMake and the libclang development package."
  (interactive
   (list (let ((command
                (format
                 (concat "%s %s %s %s && %s --build . "
                         "--use-stderr --config Release --target install")
                 (shell-quote-argument irony-cmake-executable)
                 (shell-quote-argument (concat "-DCMAKE_INSTALL_PREFIX="
                                               (expand-file-name
                                                irony-server-install-prefix)))
                 (mapconcat 'shell-quote-argument irony-extra-cmake-args " ")
                 (shell-quote-argument
                  (or irony-server-source-dir
                      (expand-file-name "server"
                                        (file-name-directory
                                         (find-library-name "irony")))))
                 (shell-quote-argument irony-cmake-executable))))
           (irony--install-server-read-command command))))
  (let ((build-dir (or irony-server-build-dir
                       (concat
                        (file-name-as-directory temporary-file-directory)
                        (file-name-as-directory (format "build-irony-server-%s"
                                                        (irony-version)))))))
    (make-directory build-dir t)
    (let ((default-directory build-dir))
      ;; we need to kill the process to be able to install a new one,
      ;; at least on Windows
      (irony-server-kill)
      (with-current-buffer (compilation-start command nil
                                              #'(lambda (maj-mode)
                                                  "*irony-server build*"))
        (setq-local compilation-finish-functions
                    '(irony--server-install-finish-function))))))

(defun irony--server-install-finish-function (buffer msg)
  (if (string= "finished\n" msg)
      (message "irony-server installed successfully!")
    (message "Failed to build irony-server, you are on your own buddy!")))

(defun irony--find-server-executable ()
  "Return the path to the irony-server executable.

Throw an `irony-server-error' if a proper executable cannot be
found."
  (let* ((exec-path (cons (expand-file-name "bin" irony-server-install-prefix)
                          exec-path))
         (exe (executable-find "irony-server")))
    (condition-case err
        (let ((version (car (process-lines exe "--version"))))
          (if (and (string-match "^irony-server version " version)
                   (version= (irony-version)
                             (substring version
                                        (length "irony-server version "))))
              ;; irony-server is working and up-to-date!
              exe
            (signal 'irony-server-error
                    (list
                     (format "irony-server version mismatch: %s"
                             (substitute-command-keys
                              "type `\\[irony-install-server]' to reinstall"))))))
      (irony-server-error
       (signal (car err) (cdr err)))
      (error
       (signal 'irony-server-error
               (if (and exe
                        (file-executable-p exe))
                   ;; failed to execute due to a runtime problem, i.e:
                   ;; libclang.so isn't in the ld paths
                   (list (format "irony-server is broken! %s"
                                 (error-message-string err)))
                 ;; irony-server doesn't exists, first time irony-mode is used?
                 ;; inform the user about how to build the executable
                 (list
                  (format "irony-server not installed! %s"
                          (substitute-command-keys
                           "Type `\\[irony-install-server]' to install")))))))))


;;
;; irony-server process management.
;;

(defvar irony--server-executable nil)
(defvar irony--server-process nil)
(defvar irony--server-buffer " *Irony*"
  "The name of the buffer for the irony process to run in.

When using a leading space, the buffer is hidden from the buffer
list (and undo information is not kept).")

(defun irony--start-server-process ()
  (unless irony--server-executable
    ;; if not found, an `irony-server-error' error is signaled
    (setq irony--server-executable (irony--find-server-executable)))
  (let ((process-connection-type nil)
        (process-adaptive-read-buffering nil)
        (w32-pipe-buffer-size (when (boundp 'w32-pipe-buffer-size)
                                (or irony-server-w32-pipe-buffer-size
                                    w32-pipe-buffer-size)))
        process)
    (setq process
          (start-process-shell-command
           "Irony"                    ;process name
           irony--server-buffer       ;buffer
           (format "%s -i 2> %s"      ;command
                   (shell-quote-argument irony--server-executable)
                   (expand-file-name
                    (format-time-string "irony.%Y-%m-%d_%Hh-%Mm-%Ss.log")
                    temporary-file-directory))))
    (set-process-query-on-exit-flag process nil)
    (irony-iotask-setup-process process)
    process))

;;;###autoload
(defun irony-server-kill ()
  "Kill the running irony-server process, if any."
  (interactive)
  (when (process-live-p irony--server-process)
    (kill-process irony--server-process)
    (setq irony--server-process nil)))

(defun irony--get-server-process-create ()
  (unless (process-live-p irony--server-process)
    (setq irony--server-process (irony--start-server-process)))
  irony--server-process)

(defun irony--run-task (task)
  (irony-iotask-run (irony--get-server-process-create) task))

(defun irony--run-task-asynchronously (task callback)
  (irony-iotask-schedule (irony--get-server-process-create) task callback))

(defun irony--quote-strings (strings &optional separator)
  "Like `combine-and-quote-strings', but when the string is \"\" or nil,
`irony--quote-strings' will convert it to \"\" instead of <SPC>.
That is:

  (irony--quote-strings \'(\"a\" \"\" \"b\"))            => \"a \\\"\\\" b\"
  (combine-and-quote-strings \'(\"a\" \"\" \"b\"))       => \"a  b\"
"
  (let* ((sep (or separator " "))
         (re (concat "[\\\"]" "\\|" (regexp-quote sep))))
    (mapconcat
     (lambda (str)
       (cond
        ((or (not str) (string= str ""))
         "\"\"")
        ((string-match re str)
         (concat "\"" (replace-regexp-in-string "[\\\"]" "\\\\\\&" str) "\""))
        (t str)))
     strings sep)))

(defun irony--server-send-command (command &rest args)
  (let ((command-line (concat (irony--quote-strings
                               (mapcar (lambda (arg)
                                         (if (numberp arg)
                                             (number-to-string arg)
                                           arg))
                                       (cons command args)))
                              "\n")))
    (irony-iotask-send-string command-line)))

;; XXX: this code can run in very tight very sensitive on big inputs,
;; every change should be measured
(defun irony--server-command-update (&rest _args)
  (when (and (>= (buffer-size) (length irony--eot))
             (string-equal (buffer-substring-no-properties
                            (- (point-max) (length irony--eot)) (point-max))
                           irony--eot))
    (condition-case-unless-debug nil
        (let ((result (read (current-buffer))))
          (cl-case (car result)
            (success
             (irony-iotask-set-result (cdr result)))
            (error
             (apply #'irony-iotask-set-error 'irony-server-error
                    (cdr result)))))
      (error
       (throw 'invalid-msg t)))))

;; FIXME: code duplication with `irony--server-command-update'
;; XXX: this code can run in very tight very sensitive on big inputs,
;; every change should be measured
(defun irony--server-query-update (&rest _args)
  (when (and (>= (buffer-size) (length irony--eot))
             (string-equal (buffer-substring-no-properties
                            (- (point-max) (length irony--eot)) (point-max))
                           irony--eot))
    (condition-case-unless-debug nil
        (irony-iotask-set-result (read (current-buffer)))
      (error
       (throw 'invalid-msg t)))))


;;
;; Server commands
;;

(irony-iotask-define-task irony--t-get-compile-options
  "`get-compile-options' server command."
  :start (lambda (build-dir file)
           (irony--server-send-command "get-compile-options" build-dir file))
  :update irony--server-command-update)

(defun irony--get-compile-options-task (build-dir file)
  (irony-iotask-package-task irony--t-get-compile-options build-dir file))

(cl-defstruct (irony--buffer-state
               (:constructor irony--buffer-state-create-1))
  file
  exists
  modified
  tick)

(defun irony--buffer-state-create (buffer)
  (let ((file (buffer-file-name buffer)))
    (irony--buffer-state-create-1 :file file
                                  :exists (and file (file-exists-p file))
                                  :modified (buffer-modified-p buffer)
                                  :tick (buffer-chars-modified-tick buffer))))

(defun irony--buffer-state-compare (old new)
  (unless (equal old new)
    (cond
     ((irony--buffer-state-modified new) 'set-unsaved)
     ((null old) nil)                   ;noop
     ((and
       (irony--buffer-state-modified old)
       (irony--buffer-state-exists old)) 'reset-unsaved))))

(irony-iotask-define-task irony--t-set-unsaved
  "`set-unsaved' server command."
  :start (lambda (process buffer buf-state)
           (let ((elem (assq buffer (process-get process :unsaved-buffers)))
                 temp-file)
             (if (eq (cdr elem) buf-state)
                 ;; early exit if already cached
                 (irony-iotask-set-result t)
               (setq temp-file (make-temp-file "irony-unsaved-"))
               (irony-iotask-put :temp-file temp-file)
               (irony-iotask-put :buffer-state buf-state)
               (process-put process :buffer-state buf-state)
               (with-current-buffer buffer
                 (irony--without-narrowing
                   (let ((write-region-inhibit-fsync t))
                     (write-region nil nil temp-file nil 0)))
                 (irony--server-send-command "set-unsaved"
                                             (irony--get-buffer-path-for-server)
                                             temp-file)))))
  :update irony--server-command-update
  :finish (lambda (&rest _args)
            (delete-file (irony-iotask-get :temp-file)))
  :on-success
  (lambda (process buffer &rest _args)
    (let* ((unsaved-buffers (process-get process :unsaved-buffers))
           (elem (assq buffer unsaved-buffers))
           (buf-state (irony-iotask-get :buffer-state)))
      (if elem
          (setcdr elem buf-state)
        (process-put process :unsaved-buffers (cons (cons buffer buf-state)
                                                    unsaved-buffers))))))

(defun irony--set-unsaved-task (process buffer buf-state)
  (irony-iotask-package-task irony--t-set-unsaved process buffer buf-state))

(irony-iotask-define-task irony--t-reset-unsaved
  "`reset-unsaved' server command."
  :start (lambda (process buffer)
           (if (assq buffer (process-get process :unsaved-buffers))
               (irony--server-send-command "reset-unsaved"
                                           (irony--get-buffer-path-for-server
                                            buffer))
             ;; exit early if already reset
             (irony-iotask-set-result t)))
  :update irony--server-command-update
  :finish (lambda (process buffer)
            (process-put
             process
             :unsaved-buffers
             (assq-delete-all buffer (process-get process :unsaved-buffers)))))

(defun irony--reset-unsaved-task (process buffer)
  (irony-iotask-package-task irony--t-reset-unsaved process buffer))

(defun irony--list-unsaved-irony-mode-buffers (&optional ignore-list)
  (delq nil (mapcar (lambda (buf)
                      (unless (memq buf ignore-list)
                        (when (buffer-modified-p buf)
                          (with-current-buffer buf
                            (and irony-mode buf)))))
                    (buffer-list))))

(defun irony--get-buffer-change-alist (process)
  "Return a list of (buffer . old-state).

old-state can be nil if the old state isn't known."
  (let ((unsaved-list (process-get process :unsaved-buffers)))
    (append unsaved-list
            (mapcar (lambda (buf)
                      (cons buf nil))
                    (irony--list-unsaved-irony-mode-buffers
                     (mapcar #'car unsaved-list))))))

(defun irony--unsaved-buffers-tasks ()
  (let ((process (irony--get-server-process-create))
        result)
    (dolist (buffer-old-state-cons (irony--get-buffer-change-alist process)
                                   result)
      (let* ((buffer (car buffer-old-state-cons))
             (old-state (cdr buffer-old-state-cons))
             (new-state (irony--buffer-state-create buffer))
             (task
              (cl-case (irony--buffer-state-compare old-state new-state)
                (set-unsaved
                 (irony--set-unsaved-task process buffer new-state))
                (reset-unsaved
                 (irony--reset-unsaved-task process buffer)))))
        (when task
          (setq result (if result
                           (irony-iotask-chain result task)
                         task)))))))

(irony-iotask-define-task irony--t-parse
  "`parse' server command."
  :start (lambda (file compile-options)
           (apply #'irony--server-send-command "parse" file "--"
                  compile-options))
  :update irony--server-command-update)

(defun irony--parse-task-1 (&optional buffer)
  (with-current-buffer (or buffer (current-buffer))
    (irony-iotask-package-task irony--t-parse
                               (irony--get-buffer-path-for-server)
                               (irony--adjust-compile-options))))

(defun irony--parse-task (&optional buffer)
  (let ((unsaved-tasks (irony--unsaved-buffers-tasks))
        (parse-task (irony--parse-task-1 buffer)))
    (if unsaved-tasks
        (irony-iotask-chain unsaved-tasks parse-task)
      parse-task)))

(irony-iotask-define-task irony--t-diagnostics
  "`parse' server command."
  :start (lambda ()
           (irony--server-send-command "diagnostics"))
  :update irony--server-query-update)

(defun irony--diagnostics-task (&optional buffer)
  (irony-iotask-chain
   (irony--parse-task buffer)
   (irony-iotask-package-task irony--t-diagnostics)))

(irony-iotask-define-task irony--t-get-type
  "`get-type' server command."
  :start (lambda (line col)
           (irony--server-send-command "get-type" line col))
  :update irony--server-query-update)

(defun irony--get-type-task (&optional buffer pos)
  (let ((line-column (irony--completion-line-column pos)))
    (irony-iotask-chain
     (irony--parse-task buffer)
     (irony-iotask-package-task irony--t-get-type
                                (car line-column) (cdr line-column)))))

;;;###autoload
(defun irony-get-type ()
  "Get the type of symbol under cursor."
  (interactive)
  (let ((types (irony--run-task (irony--get-type-task))))
    (unless types
      (user-error "Type not found"))
    (if (and (cdr types) (not (string= (car types) (cadr types))))
        (message "%s (aka '%s')" (car types) (cadr types))
      (message "%s" (car types)))))

(defun irony-parse-buffer-async (&optional callback)
  "Parse the current buffer sending results to an optional
  CALLBACK function."
  (irony--run-task-asynchronously (irony--parse-task)
                                  (or callback #'ignore)))

(provide 'irony)

;; Local Variables:
;; byte-compile-warnings: (not cl-functions)
;; End:

;;; irony.el ends here