;;; ycmd.el --- emacs bindings to the ycmd completion server -*- lexical-binding: t -*-
;;
;; Copyright (c) 2014-2017 Austin Bingham, Peter Vasil
;;
;; Authors: Austin Bingham <austin.bingham@gmail.com>
;;          Peter Vasil <mail@petervasil.net>
;; Version: 1.3-cvs
;; URL: https://github.com/abingham/emacs-ycmd
;; Package-Requires: ((emacs "24.4") (dash "2.13.0") (s "1.11.0") (deferred "0.5.1") (cl-lib "0.6.1") (let-alist "1.0.5") (request "0.3.0") (request-deferred "0.3.0") (pkg-info "0.6"))
;;
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;;
;; Description:
;;
;; ycmd is a modular server that provides completion for C/C++/ObjC
;; and Python, among other languages. This module provides an emacs
;; client for that server.
;;
;; ycmd is a bit peculiar in a few ways. First, communication with the
;; server uses HMAC to authenticate HTTP messages. The server is
;; started with an HMAC secret that the client uses to generate hashes
;; of the content it sends. Second, the server gets this HMAC
;; information (as well as other configuration information) from a
;; file that the server deletes after reading. So when the code in
;; this module starts a server, it has to create a file containing the
;; secret code. Since the server deletes this file, this code has to
;; create a new one for each server it starts. Hopefully by knowing
;; this, you'll be able to make more sense of some of what you see
;; below.
;;
;; For more details, see the project page at
;; https://github.com/abingham/emacs-ycmd.
;;
;; Installation:
;;
;; Copy this file to to some location in your emacs load path. Then add
;; "(require 'ycmd)" to your emacs initialization (.emacs,
;; init.el, or something).
;;
;; Example config:
;;
;;   (require 'ycmd)
;;   (ycmd-setup)
;;
;; Basic usage:
;;
;; First you'll want to configure a few things. If you've got a global
;; ycmd config file, you can specify that with `ycmd-global-config':
;;
;;   (set-variable 'ycmd-global-config "/path/to/global_conf.py")
;;
;; Then you'll want to configure your "extra-config whitelist"
;; patterns. These patterns determine which extra-conf files will get
;; loaded automatically by ycmd. So, for example, if you want to make
;; sure that ycmd will automatically load all of the extra-conf files
;; underneath your "~/projects" directory, do this:
;;
;;   (set-variable 'ycmd-extra-conf-whitelist '("~/projects/*"))
;;
;; Now, the first time you open a file for which ycmd can perform
;; completions, a ycmd server will be automatically started.
;;
;; When ycmd encounters an extra-config that's not on the white list,
;; it checks `ycmd-extra-conf-handler' to determine what to do. By
;; default this is set to `ask', in which case the user is asked
;; whether to load the file or ignore it. You can also set it to
;; `load', in which case all extra-confs are loaded (and you don't
;; really need to worry about `ycmd-extra-conf-whitelist'.) Or you can
;; set this to `ignore', in which case all extra-confs are
;; automatically ignored.
;;
;; Use `ycmd-get-completions' to get completions at some point in a
;; file. For example:
;;
;;   (ycmd-get-completions buffer position)
;;
;; You can use `ycmd-display-completions' to toy around with completion
;; interactively and see the shape of the structures in use.
;;
;;; License:
;;
;; Permission is hereby granted, free of charge, to any person
;; obtaining a copy of this software and associated documentation
;; files (the "Software"), to deal in the Software without
;; restriction, including without limitation the rights to use, copy,
;; modify, merge, publish, distribute, sublicense, and/or sell copies
;; of the Software, and to permit persons to whom the Software is
;; furnished to do so, subject to the following conditions:
;;
;; The above copyright notice and this permission notice shall be
;; included in all copies or substantial portions of the Software.
;;
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
;; BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
;; ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
;; CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
;; SOFTWARE.

;;; Code:

(eval-when-compile
  (require 'cl-lib)
  (require 'let-alist))
(require 'dash)
(require 's)
(require 'deferred)
(require 'hmac-def)
(require 'json)
(require 'request)
(require 'request-deferred)
(require 'etags)
(require 'easymenu)
(require 'diff)
(require 'diff-mode)

(declare-function pkg-info-version-info "pkg-info" (package))

(defgroup ycmd nil
  "a ycmd emacs client"
  :link '(url-link :tag "Github" "https://github.com/abingham/emacs-ycmd")
  :group 'tools
  :group 'programming)

(defcustom ycmd-global-config nil
  "Path to global extra conf file."
  :type '(string))

(defcustom ycmd-extra-conf-whitelist nil
  "List of glob expressions which match extra configs.
Whitelisted configs are loaded without confirmation."
  :type '(repeat string))

(defcustom ycmd-extra-conf-handler 'ask
  "What to do when an un-whitelisted extra config is encountered.

Options are:

`load'
      Automatically load unknown extra confs.

`ignore'
     Ignore unknown extra confs and do not load them.

`ask'
     Ask the user for each unknown extra conf."
  :type '(choice (const :tag "Load unknown extra confs" load)
                 (const :tag "Ignore unknown extra confs" ignore)
                 (const :tag "Ask the user" ask))
  :risky t)

(defcustom ycmd-host "127.0.0.1"
  "The host on which the ycmd server is running."
  :type '(string))

(defcustom ycmd-server-command nil
  "The ycmd server program command.

The value is a list of arguments to run the ycmd server.
Example value:

\(set-variable 'ycmd-server-command (\"python\" \"/path/to/ycmd/package/\"))"
  :type '(repeat string))

(defcustom ycmd-server-args '("--log=debug"
                              "--keep_logfile"
                              "--idle_suicide_seconds=10800")
  "Extra arguments to pass to the ycmd server."
  :type '(repeat string))

(defcustom ycmd-server-port nil
  "The ycmd server port.  If nil, use random port."
  :type '(number))

(defcustom ycmd-file-parse-result-hook nil
  "Functions to run with file-parse results.

Each function will be called with with the results returned from
ycmd when it parses a file in response to
/event_notification."
  :type 'hook
  :risky t)

(defcustom ycmd-idle-change-delay 0.5
  "Number of seconds to wait after buffer modification before
re-parsing the contents."
  :type '(number)
  :safe #'numberp)

(defcustom ycmd-keepalive-period 600
  "Number of seconds between keepalive messages."
  :type '(number))

(defcustom ycmd-startup-timeout 3
  "Number of seconds to wait for the server to start."
  :type '(number))

(defcustom ycmd-delete-process-delay 3
  "Seconds to wait for the server to finish before killing the process."
  :type '(number))

(defcustom ycmd-parse-conditions '(save new-line mode-enabled)
  "When ycmd should reparse the buffer.

The variable is a list of events that may trigger parsing the
buffer for new completion:

`save'
      Set buffer-needs-parse flag after the buffer was saved.

`new-line'
      Set buffer-needs-parse flag immediately after a new
      line was inserted into the buffer.

`idle-change'
      Set buffer-needs-parse flag a short time after a
      buffer has changed.  (See `ycmd-idle-change-delay')

`mode-enabled'
      Set buffer-needs-parse flag after `ycmd-mode' has been
      enabled.

`buffer-focus'
      Set buffer-needs-parse flag when an unparsed buffer gets
      focus.

If nil, never set buffer-needs-parse flag.  For a manual reparse,
use `ycmd-parse-buffer'."
  :type '(set (const :tag "After the buffer was saved" save)
              (const :tag "After a new line was inserted" new-line)
              (const :tag "After a buffer was changed and idle" idle-change)
              (const :tag "After a `ycmd-mode' was enabled" mode-enabled)
              (const :tag "After an unparsed buffer gets focus" buffer-focus))
  :safe #'listp)

(defcustom ycmd-default-tags-file-name "tags"
  "The default tags file name."
  :type 'string)

(defcustom ycmd-force-semantic-completion nil
  "Whether to use always semantic completion."
  :type 'boolean)

(defcustom ycmd-auto-trigger-semantic-completion t
  "If non-nil, semantic completion is turned off.
Semantic completion is still available if
`ycmd-force-semantic-completion' is non-nil."
  :type 'boolean)

(defcustom ycmd-hide-url-status t
  "Whether to quash url status messages for ycmd requests."
  :type 'boolean)

(defcustom ycmd-bypass-url-proxy-services t
  "Bypass proxies for local traffic with the ycmd server.

If non-nil, bypass the variable `url-proxy-services' in
`ycmd--request' by setting it to nil and add `no_proxy' to
`process-environment' to bypass proxies when using `curl' as
`request-backend' and for the ycmd process."
  :type 'boolean)

(defcustom ycmd-tag-files nil
  "Whether to collect identifiers from tags file.

nil
    Do not collect identifiers from tag files.

`auto'
    Look up directory hierarchy for first found tags file with
    `ycmd-default-tags-file-name'.

string
    A tags file name.

list
    A list of tag file names."
  :type '(choice (const :tag "Don't use tag file." nil)
                 (const :tag "Locate tags file automatically" auto)
                 (string :tag "Tag file name")
                 (repeat :tag "List of tag files"
                         (string :tag "Tag file name")))
  :safe (lambda (obj)
          (or (symbolp obj)
              (stringp obj)
              (ycmd--string-list-p obj))))

(defcustom ycmd-file-type-map
  '((c++-mode . ("cpp"))
    (c-mode . ("c"))
    (caml-mode . ("ocaml"))
    (csharp-mode . ("cs"))
    (d-mode . ("d"))
    (erlang-mode . ("erlang"))
    (emacs-lisp-mode . ("elisp"))
    (go-mode . ("go"))
    (java-mode . ("java"))
    (js-mode . ("javascript"))
    (js2-mode . ("javascript"))
    (lua-mode . ("lua"))
    (objc-mode . ("objc"))
    (perl-mode . ("perl"))
    (cperl-mode . ("perl"))
    (php-mode . ("php"))
    (python-mode . ("python"))
    (ruby-mode . ("ruby"))
    (rust-mode . ("rust"))
    (swift-mode . ("swift"))
    (scala-mode . ("scala"))
    (tuareg-mode . ("ocaml"))
    (typescript-mode . ("typescript")))
  "Mapping from major modes to ycmd file-type strings.

Used to determine a) which major modes we support and b) how to
describe them to ycmd."
  :type '(alist :key-type symbol :value-type (repeat string)))

(defcustom ycmd-min-num-chars-for-completion 2
  "The minimum number of characters for identifier completion.

It controls the number of characters the user needs to type
before identifier-based completion suggestions are triggered.

This option is NOT used for semantic completion.

Setting this it to a high number like 99 effectively turns off
the identifier completion engine and just leaves the semantic
engine."
  :type 'integer)

(defcustom ycmd-max-num-identifier-candidates 10
  "The maximum number of identifier completion results."
  :type 'integer)

(defcustom ycmd-seed-identifiers-with-keywords nil
  "Whether to seed identifier database with keywords."
  :type 'boolean)

(defcustom ycmd-get-keywords-function 'ycmd--get-keywords-from-alist
  "Function to get keywords for current mode."
  :type 'symbol)

(defcustom ycmd-gocode-binary-path nil
  "Gocode binary path."
  :type 'string)

(defcustom ycmd-godef-binary-path nil
  "Godef binary path."
  :type 'string)

(defcustom ycmd-rust-src-path nil
  "Rust source path."
  :type 'string)

(defcustom ycmd-swift-src-path nil
  "Swift source path."
  :type 'string)

(defcustom ycmd-racerd-binary-path nil
  "Racerd binary path."
  :type 'string)

(defcustom ycmd-python-binary-path nil
  "Python binary path."
  :type 'string)

(defcustom ycmd-global-modes t
  "Modes for which `ycmd-mode' is turned on by `global-ycmd-mode'.

If t, ycmd mode is turned on for all major modes in
`ycmd-file-type-map'.  If set to all, ycmd mode is turned on
for all major-modes.  If a list, ycmd mode is turned on for all
`major-mode' symbols in that list.  If the `car' of the list is
`not', ycmd mode is turned on for all `major-mode' symbols _not_
in that list.  If nil, ycmd mode is never turned on by
`global-ycmd-mode'."
  :type '(choice (const :tag "none" nil)
                 (const :tag "member in `ycmd-file-type-map'" t)
                 (const :tag "all" all)
                 (set :menu-tag "mode specific" :tag "modes"
                      :value (not)
                      (const :tag "Except" not)
                      (repeat :inline t (symbol :tag "mode")))))

(defcustom ycmd-confirm-fixit t
  "Whether to confirm when applying fixit on line."
  :type 'boolean)

(defcustom ycmd-after-exception-hook nil
  "Function to run if server request resulted in exception.

This hook is run whenever an exception is thrown after a ycmd
server request.  Four arguments are passed to the function, a
string with the type of request that triggerd the exception, the
buffer and the point at the time of the request and the server
response structure which looks like this:

  ((exception
    (TYPE . \"RuntimeError\"))
   (traceback . \"long traceback string\")
   (message . \"Can't jump to definition.\"))

This variable is a normal hook.  See Info node `(elisp)Hooks'."
  :type 'hook
  :risky t)

(defcustom ycmd-after-teardown-hook nil
  "Functions to run after execution of `ycmd--teardown'.

This variable is a normal hook.  See Info node `(elisp)Hooks'."
  :type 'hook
  :risky t)

(defcustom ycmd-mode-line-prefix "ycmd"
  "Base mode line lighter for ycmd."
  :type 'string
  :package-version '(ycmd . "1.3"))

(defcustom ycmd-completing-read-function #'completing-read
  "Function to read from minibuffer with completion.

The function must be compatible to the built-in `completing-read'
function."
  :type '(choice (const :tag "Default" completing-read)
                 (const :tag "IDO" ido-completing-read)
                 (function :tag "Custom function"))
  :risky t
  :package-version '(ycmd . "1.2"))

(defconst ycmd--file-types-with-diagnostics
  '("c"
    "cpp"
    "objc"
    "objcpp"
    "cs"
    "typescript")
  "A list of ycmd file type strings which support semantic diagnostics.")

(defvar ycmd-keywords-alist
  '((c++-mode
     "alignas" "alignof" "and" "and_eq" "asm" "auto" "bitand" "bitor" "bool"
     "break" "case" "catch" "char" "char16_t" "char32_t" "class" "compl"
     "concept" "const" "const_cast" "constexpr" "continue" "decltype" "default"
     "define" "defined" "delete" "do" "double" "dynamic_cast" "elif" "else"
     "endif" "enum" "error" "explicit" "export" "extern" "false" "final" "float"
     "for" "friend" "goto" "if" "ifdef" "ifndef" "include" "inline" "int" "line"
     "long" "mutable" "namespace" "new" "noexcept" "not" "not_eq" "nullptr"
     "operator" "or" "or_eq" "override" "pragma" "_Pragma" "private" "protected"
     "public" "register" "reinterpret_cast" "requires" "return" "short" "signed"
     "sizeof" "static" "static_assert" "static_cast" "struct" "switch"
     "template" "this" "thread_local" "throw" "true" "try" "typedef" "typeid"
     "typename" "union" "unsigned" "using" "virtual" "void" "volatile" "wchar_t"
     "while" "xor" "xor_eq")
    (c-mode
     "auto" "_Alignas" "_Alignof" "_Atomic" "_Bool" "break" "case" "char"
     "_Complex" "const" "continue" "default" "define" "defined" "do" "double"
     "elif" "else" "endif" "enum" "error" "extern" "float" "for" "goto"
     "_Generic" "if" "ifdef" "ifndef" "_Imaginary" "include" "inline" "int"
     "line" "long" "_Noreturn" "pragma" "register" "restrict" "return" "short"
     "signed" "sizeof" "static" "struct" "switch" "_Static_assert" "typedef"
     "_Thread_local" "undef" "union" "unsigned" "void" "volatile" "while")
    (go-mode
     "break" "case" "chan" "const" "continue" "default" "defer" "else"
     "fallthrough" "for" "func" "go" "goto" "if" "import" "interface" "map"
     "package" "range" "return" "select" "struct" "switch" "type" "var")
    (lua-mode
     "and" "break" "do" "else" "elseif" "end" "false" "for" "function" "if" "in"
     "local" "nil" "not" "or" "repeat" "return" "then" "true" "until" "while")
    (python-mode
     "ArithmeticError" "AssertionError" "AttributeError" "BaseException"
     "BufferError" "BytesWarning" "DeprecationWarning" "EOFError" "Ellipsis"
     "EnvironmentError" "Exception" "False" "FloatingPointError" "FutureWarning"
     "GeneratorExit" "IOError" "ImportError" "ImportWarning" "IndentationError"
     "IndexError" "KeyError" "KeyboardInterrupt" "LookupError" "MemoryError"
     "NameError" "None" "NotImplemented" "NotImplementedError" "OSError"
     "OverflowError" "PendingDeprecationWarning" "ReferenceError" "RuntimeError"
     "RuntimeWarning" "StandardError" "StopIteration" "SyntaxError"
     "SyntaxWarning" "SystemError" "SystemExit" "TabError" "True" "TypeError"
     "UnboundLocalError" "UnicodeDecodeError" "UnicodeEncodeError"
     "UnicodeError" "UnicodeTranslateError" "UnicodeWarning" "UserWarning"
     "ValueError" "Warning" "ZeroDivisionError" "__builtins__" "__debug__"
     "__doc__" "__file__" "__future__" "__import__" "__init__" "__main__"
     "__name__" "__package__" "_dummy_thread" "_thread" "abc" "abs" "aifc" "all"
     "and" "any" "apply" "argparse" "array" "as" "assert" "ast" "asynchat"
     "asyncio" "asyncore" "atexit" "audioop" "base64" "basestring" "bdb" "bin"
     "binascii" "binhex" "bisect" "bool" "break" "buffer" "builtins" "bytearray"
     "bytes" "bz2" "calendar" "callable" "cgi" "cgitb" "chr" "chuck" "class"
     "classmethod" "cmath" "cmd" "cmp" "code" "codecs" "codeop" "coerce"
     "collections" "colorsys" "compile" "compileall" "complex" "concurrent"
     "configparser" "contextlib" "continue" "copy" "copyreg" "copyright"
     "credits" "crypt" "csv" "ctypes" "curses" "datetime" "dbm" "decimal" "def"
     "del" "delattr" "dict" "difflib" "dir" "dis" "distutils" "divmod" "doctest"
     "dummy_threading" "elif" "else" "email" "enumerate" "ensurepip" "enum"
     "errno" "eval" "except" "exec" "execfile" "exit" "faulthandler" "fcntl"
     "file" "filecmp" "fileinput" "filter" "finally" "float" "fnmatch" "for"
     "format" "formatter" "fpectl" "fractions" "from" "frozenset" "ftplib"
     "functools" "gc" "getattr" "getopt" "getpass" "gettext" "glob" "global"
     "globals" "grp" "gzip" "hasattr" "hash" "hashlib" "heapq" "help" "hex"
     "hmac" "html" "http" "id" "if" "imghdr" "imp" "impalib" "import"
     "importlib" "in" "input" "inspect" "int" "intern" "io" "ipaddress" "is"
     "isinstance" "issubclass" "iter" "itertools" "json" "keyword" "lambda"
     "len" "license" "linecache" "list" "locale" "locals" "logging" "long"
     "lzma" "macpath" "mailbox" "mailcap" "map" "marshal" "math" "max"
     "memoryview" "mimetypes" "min" "mmap" "modulefinder" "msilib" "msvcrt"
     "multiprocessing" "netrc" "next" "nis" "nntplib" "not" "numbers" "object"
     "oct" "open" "operator" "optparse" "or" "ord" "os" "ossaudiodev" "parser"
     "pass" "pathlib" "pdb" "pickle" "pickletools" "pipes" "pkgutil" "platform"
     "plistlib" "poplib" "posix" "pow" "pprint" "print" "profile" "property"
     "pty" "pwd" "py_compiler" "pyclbr" "pydoc" "queue" "quit" "quopri" "raise"
     "random" "range" "raw_input" "re" "readline" "reduce" "reload" "repr"
     "reprlib" "resource" "return" "reversed" "rlcompleter" "round" "runpy"
     "sched" "select" "selectors" "self" "set" "setattr" "shelve" "shlex"
     "shutil" "signal" "site" "slice" "smtpd" "smtplib" "sndhdr" "socket"
     "socketserver" "sorted" "spwd" "sqlite3" "ssl" "stat" "staticmethod"
     "statistics" "str" "string" "stringprep" "struct" "subprocess" "sum"
     "sunau" "super" "symbol" "symtable" "sys" "sysconfig" "syslog" "tabnanny"
     "tarfile" "telnetlib" "tempfile" "termios" "test" "textwrap" "threading"
     "time" "timeit" "tkinter" "token" "tokenize" "trace" "traceback"
     "tracemalloc" "try" "tty" "tuple" "turtle" "type" "types" "unichr"
     "unicode" "unicodedata" "unittest" "urllib" "uu" "uuid" "vars" "venv"
     "warnings" "wave" "weakref" "webbrowser" "while" "winsound" "winreg" "with"
     "wsgiref" "xdrlib" "xml" "xmlrpc" "xrange" "yield" "zip" "zipfile" "zipimport"
     "zlib")
    (rust-mode
     "Self"
     "as" "box" "break" "const" "continue" "crate" "else" "enum" "extern"
     "false" "fn" "for" "if" "impl" "in" "let" "loop" "macro" "match" "mod"
     "move" "mut" "pub" "ref" "return" "self" "static" "struct" "super"
     "trait" "true" "type" "unsafe" "use" "where" "while")
    (swift-mode
     "true" "false" "nil" "#available" "#colorLiteral" "#column" "#else"
     "#elseif" "#endif" "#fileLiteral" "#file" "#function" "#if" "#imageLiteral"
     "#keypath" "#line" "#selector" "#sourceLocation" "associatedtype" "class"
     "deinit" "enum" "extension" "fileprivate" "func" "import" "init" "inout"
     "internal" "let" "open" "operator" "private" "protocol" "public" "static"
     "struct" "subscript" "typealias" "var" "break" "case" "continue" "default"
     "defer" "do" "else" "fallthrough" "for" "guard" "if" "in" "repeat" "return"
     "switch" "where" "while" "as" "catch" "dynamicType" "is" "rethrows" "super"
     "self" "Self" "throws" "throw" "try" "Protocol" "Type" "and" "assignment"
     "associativity" "convenience" "didSet" "dynamic" "final" "get" "higherThan"
     "indirect" "infix" "lazy" "left" "lowerThan" "mutating" "none"
     "nonmutating" "optional" "override" "postfix" "precedence"
     "precedencegroup" "prefix" "required" "right" "set" "unowned" "weak"
     "willSet"))
  "Alist mapping major-modes to keywords for.

Keywords source: https://github.com/auto-complete/auto-complete/tree/master/dict
and `company-keywords'.")

(defvar ycmd--server-actual-port nil
  "The actual port being used by the ycmd server.
This is set based on the value of `ycmd-server-port' if set, or
the value from the output of the server itself.")

(defvar ycmd--hmac-secret nil
  "This is populated with the hmac secret of the current connection.
Users should never need to modify this, hence the defconst.  It is
not, however, treated as a constant by this code.  This value gets
set in ycmd-open.")

(defconst ycmd--server-process-name "ycmd-server"
  "The Emacs name of the server process.
This is used by functions like `start-process', `get-process'
and `delete-process'.")

(defvar-local ycmd--notification-timer nil
  "Timer for notifying ycmd server to do work, e.g. parsing files.")

(defvar ycmd--keepalive-timer nil
  "Timer for sending keepalive messages to the server.")

(defvar ycmd--on-focus-timer nil
  "Timer for deferring ycmd server notification to parse a buffer.")

(defvar ycmd--server-timeout-timer nil)

(defconst ycmd--server-buffer-name "*ycmd-server*"
  "Name of the ycmd server buffer.")

(defvar-local ycmd--last-status-change 'unparsed
  "The last status of the current buffer.")

(defvar-local ycmd--last-modified-tick nil
  "The BUFFER's last FileReadyToParse tick counter.")

(defvar-local ycmd--buffer-visit-flag nil)

(defvar ycmd--available-completers (make-hash-table :test 'eq))

(defvar ycmd--process-environment nil)

(defvar ycmd--mode-keywords-loaded nil
  "List of modes for which keywords have been loaded.")

(defconst ycmd-hooks-alist
  '((after-save-hook                  . ycmd--on-save)
    (after-change-functions           . ycmd--on-change)
    (window-configuration-change-hook . ycmd--on-window-configuration-change)
    (kill-buffer-hook                 . ycmd--on-close-buffer)
    (before-revert-hook               . ycmd--teardown)
    (post-command-hook                . ycmd--perform-deferred-parse))
  "Hooks which ycmd hooks in.")

(add-hook 'kill-emacs-hook 'ycmd-close)

(defvar ycmd-command-map
  (let ((map (make-sparse-keymap)))
    (define-key map "p" 'ycmd-parse-buffer)
    (define-key map "o" 'ycmd-open)
    (define-key map "c" 'ycmd-close)
    (define-key map "." 'ycmd-goto)
    (define-key map "gi" 'ycmd-goto-include)
    (define-key map "gd" 'ycmd-goto-definition)
    (define-key map "gD" 'ycmd-goto-declaration)
    (define-key map "gm" 'ycmd-goto-implementation)
    (define-key map "gp" 'ycmd-goto-imprecise)
    (define-key map "gr" 'ycmd-goto-references)
    (define-key map "gt" 'ycmd-goto-type)
    (define-key map "s" 'ycmd-toggle-force-semantic-completion)
    (define-key map "v" 'ycmd-show-debug-info)
    (define-key map "V" 'ycmd-version)
    (define-key map "d" 'ycmd-show-documentation)
    (define-key map "C" 'ycmd-clear-compilation-flag-cache)
    (define-key map "O" 'ycmd-restart-semantic-server)
    (define-key map "t" 'ycmd-get-type)
    (define-key map "T" 'ycmd-get-parent)
    (define-key map "f" 'ycmd-fixit)
    (define-key map "r" 'ycmd-refactor-rename)
    (define-key map "x" 'ycmd-completer)
    map)
  "Keymap for `ycmd-mode' interactive commands.")

(defcustom ycmd-keymap-prefix (kbd "C-c Y")
  "Prefix for key bindings of `ycmd-mode'.

Changing this variable outside Customize does not have any
effect.  To change the keymap prefix from Lisp, you need to
explicitly re-define the prefix key:

    (define-key ycmd-mode-map ycmd-keymap-prefix nil)
    (setq ycmd-keymap-prefix (kbd \"C-c ,\"))
    (define-key ycmd-mode-map ycmd-keymap-prefix
                ycmd-command-map)"
  :type 'string
  :risky t
  :set
  (lambda (variable key)
    (when (and (boundp variable) (boundp 'ycmd-mode-map))
      (define-key ycmd-mode-map (symbol-value variable) nil)
      (define-key ycmd-mode-map key ycmd-command-map))
    (set-default variable key)))

(defvar ycmd-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map ycmd-keymap-prefix ycmd-command-map)
    map)
  "Keymap for `ycmd-mode'.")

(easy-menu-define ycmd-mode-menu ycmd-mode-map
  "Menu used when `ycmd-mode' is active."
  '("YCMd"
    ["Start server" ycmd-open]
    ["Stop server" ycmd-close]
    "---"
    ["Parse buffer" ycmd-parse-buffer]
    "---"
    ["GoTo" ycmd-goto]
    ["GoToDefinition" ycmd-goto-definition]
    ["GoToDeclaration" ycmd-goto-declaration]
    ["GoToInclude" ycmd-goto-include]
    ["GoToImplementation" ycmd-goto-implementation]
    ["GoToReferences" ycmd-goto-references]
    ["GoToType" ycmd-goto-type]
    ["GoToImprecise" ycmd-goto-imprecise]
    "---"
    ["Show documentation" ycmd-show-documentation]
    ["Show type" ycmd-get-type]
    ["Show parent" ycmd-get-parent]
    "---"
    ["FixIt" ycmd-fixit]
    ["RefactorRename" ycmd-refactor-rename]
    "---"
    ["Load extra config" ycmd-load-conf-file]
    ["Restart semantic server" ycmd-restart-semantic-server]
    ["Clear compilation flag cache" ycmd-clear-compilation-flag-cache]
    ["Force semantic completion" ycmd-toggle-force-semantic-completion
     :style toggle :selected ycmd-force-semantic-completion]
    "---"
    ["Show debug info" ycmd-show-debug-info]
    ["Show version" ycmd-version t]
    ["Log enabled" ycmd-toggle-log-enabled
     :style toggle :selected ycmd--log-enabled]))

(defmacro ycmd--kill-timer (timer)
  "Cancel TIMER."
  `(when ,timer
     (cancel-timer ,timer)
     (setq ,timer nil)))

(defun ycmd-parsing-in-progress-p ()
  "Return t if parsing is in progress."
  (eq ycmd--last-status-change 'parsing))

(defun ycmd--report-status (status)
  "Report ycmd STATUS."
  (setq ycmd--last-status-change status)
  (force-mode-line-update))

(defun ycmd--mode-line-status-text ()
  "Get text for the mode line."
  (let ((force-semantic
         (when ycmd-force-semantic-completion "/s"))
        (text (pcase ycmd--last-status-change
                (`parsed "")
                (`parsing "*")
                (`unparsed "?")
                (`stopped "-")
                (`starting ">")
                (`errored "!"))))
    (concat " " ycmd-mode-line-prefix force-semantic text)))

;;;###autoload
(define-minor-mode ycmd-mode
  "Minor mode for interaction with the ycmd completion server.

When called interactively, toggle `ycmd-mode'.  With prefix ARG,
enable `ycmd-mode' if ARG is positive, otherwise disable it.

When called from Lisp, enable `ycmd-mode' if ARG is omitted,
nil or positive.  If ARG is `toggle', toggle `ycmd-mode'.
Otherwise behave as if called interactively.

\\{ycmd-mode-map}"
  :init-value nil
  :keymap ycmd-mode-map
  :lighter (:eval (ycmd--mode-line-status-text))
  :group 'ycmd
  :require 'ycmd
  :after-hook
  (progn (unless (ycmd-running-p) (ycmd-open))
         (ycmd--conditional-parse 'mode-enabled 'deferred))
  (cond
   (ycmd-mode
    (dolist (hook ycmd-hooks-alist)
      (add-hook (car hook) (cdr hook) nil 'local)))
   (t
    (dolist (hook ycmd-hooks-alist)
      (remove-hook (car hook) (cdr hook) 'local))
    (ycmd--teardown))))

;;;###autoload
(defun ycmd-setup ()
  "Setup `ycmd-mode'.

Hook `ycmd-mode' into modes in `ycmd-file-type-map'."
  (interactive)
  (dolist (it ycmd-file-type-map)
    (add-hook (intern (format "%s-hook" (symbol-name (car it)))) 'ycmd-mode)))
(make-obsolete 'ycmd-setup 'global-ycmd-mode "1.0")

(defun ycmd-version (&optional show-version)
  "Get the `emacs-ycmd' version as string.

If called interactively or if SHOW-VERSION is non-nil, show the
version in the echo area and the messages buffer.

The returned string includes both, the version from package.el
and the library version, if both a present and different.

If the version number could not be determined, signal an error,
if called interactively, or if SHOW-VERSION is non-nil, otherwise
just return nil."
  (interactive (list t))
  (if (require 'pkg-info nil :no-error)
      (let ((version (pkg-info-version-info 'ycmd)))
        (when show-version
          (message "emacs-ycmd version: %s" version))
        version)
    (error "Cannot determine version without package pkg-info")))

(defun ycmd--maybe-enable-mode ()
  "Enable `ycmd-mode' according `ycmd-global-modes'."
  (when (pcase ycmd-global-modes
          (`t (ycmd-major-mode-to-file-types major-mode))
          (`all t)
          (`(not . ,modes) (not (memq major-mode modes)))
          (modes (memq major-mode modes)))
    (ycmd-mode)))

;;;###autoload
(define-globalized-minor-mode global-ycmd-mode ycmd-mode
  ycmd--maybe-enable-mode
  :init-value nil)

(defun ycmd-unload-function ()
  "Unload function for ycmd."
  (global-ycmd-mode -1)
  (remove-hook 'kill-emacs-hook #'ycmd-close))

(defvar-local ycmd--deferred-parse nil
  "If non-nil, a deferred file parse notification is pending.")

(defun ycmd--must-defer-parse ()
  "Determine whether parsing the file has to be deferred.

Return t if parsing is to be deferred, or nil otherwise."
  (or (not (ycmd--server-alive-p))
      (not (get-buffer-window))
      (ycmd-parsing-in-progress-p)
      revert-buffer-in-progress-p))

(defun ycmd--deferred-parse-p ()
  "Return non-nil if current buffer has a deferred parse."
  ycmd--deferred-parse)

(defun ycmd--parse-deferred ()
  "Defer parse notification for current buffer."
  (setq ycmd--deferred-parse t))

(defun ycmd--perform-deferred-parse ()
  "Perform the deferred parse."
  (when (ycmd--deferred-parse-p)
    (setq ycmd--deferred-parse nil)
    (ycmd--conditional-parse)))

(defun ycmd--conditional-parse (&optional condition force-deferred)
  "Reparse the buffer under CONDITION.

If CONDITION is non-nil, determine whether a ready to parse
notification should be sent according `ycmd-parse-conditions'.

If FORCE-DEFERRED is non-nil perform parse notification later."
  (when (and ycmd-mode
             (or (not condition)
                 (memq condition ycmd-parse-conditions)))
    (if (or force-deferred (ycmd--must-defer-parse))
        (ycmd--parse-deferred)
      (let ((buffer (current-buffer)))
        (deferred:$
          (deferred:next
            (lambda ()
              (with-current-buffer buffer
                (ycmd--on-visit-buffer))))
          (deferred:nextc it
            (lambda ()
              (with-current-buffer buffer
                (let ((tick (buffer-chars-modified-tick)))
                  (unless (equal tick ycmd--last-modified-tick)
                    (setq ycmd--last-modified-tick tick)
                    (ycmd-notify-file-ready-to-parse)))))))))))

(defun ycmd--on-save ()
  "Function to run when the buffer has been saved."
  (ycmd--conditional-parse 'save))

(defun ycmd--on-idle-change ()
  "Function to run on idle-change."
  (ycmd--kill-timer ycmd--notification-timer)
  (ycmd--conditional-parse 'idle-change))

(defun ycmd--on-change (beg end _len)
  "Function to run when a buffer change between BEG and END.
_LEN is ununsed."
  (save-match-data
    (when ycmd-mode
      (ycmd--kill-timer ycmd--notification-timer)
      (if (string-match-p "\n" (buffer-substring beg end))
          (ycmd--conditional-parse 'new-line)
        (setq ycmd--notification-timer
              (run-at-time ycmd-idle-change-delay nil
                           #'ycmd--on-idle-change))))))

(defun ycmd--on-unparsed-buffer-focus (buffer)
  "Function to run when an unparsed BUFFER gets focus."
  (ycmd--kill-timer ycmd--on-focus-timer)
  (with-current-buffer buffer
    (ycmd--conditional-parse 'buffer-focus)))

(defun ycmd--on-window-configuration-change ()
  "Function to run by `window-configuration-change-hook'."
  (if (ycmd--deferred-parse-p)
      (ycmd--perform-deferred-parse)
    (when (and ycmd-mode
               (pcase ycmd--last-status-change
                 ((or `unparsed `starting) t))
               (memq 'buffer-focus ycmd-parse-conditions))
      (ycmd--kill-timer ycmd--on-focus-timer)
      (let ((on-buffer-focus-fn
             (apply-partially 'ycmd--on-unparsed-buffer-focus
                              (current-buffer))))
        (setq ycmd--on-focus-timer
              (run-at-time 1.0 nil on-buffer-focus-fn))))))

(defmacro ycmd--with-all-ycmd-buffers (&rest body)
  "Execute BODY with each `ycmd-mode' enabled buffer."
  (declare (indent 0) (debug t))
  `(dolist (buffer (buffer-list))
     (with-current-buffer buffer
       (when ycmd-mode
         ,@body))))

(defun ycmd--exception-p (response)
  "Check whether RESPONSE is an exception."
  (and (listp response) (assq 'exception response)))

(cl-defmacro ycmd-with-handled-server-exceptions (request
                                                  &rest body
                                                  &key
                                                  dont-show-exception-msg
                                                  on-exception-form
                                                  return-form
                                                  bind-current-buffer
                                                  &allow-other-keys)
  "Run a deferred REQUEST and exectute BODY on success. Catch all
exceptions raised through server communication. If it is raised
because of a unknown .ycm_extra_conf.py file, load the file or
ignore it after asking the user. Otherwise print exception to
minibuffer if NO-EXCEPTION-MESSAGE is nil. ON-EXCEPTION-FORM is
run if an exception occurs. The value of RETURN-FORM is returned
on exception. If BIND-CURRENT-BUFFER is non-nil, bind
`current-buffer' to `request-buffer' var.

\(fn REQUEST &key NO-DISPLAY ERROR-FORM RETURN-FORM
BIND-CURRENT-BUFFER &rest BODY)"
  (declare (indent 1) (debug t))
  (while (keywordp (car body))
    (setq body (cdr (cdr body))))
  `(let ((request-buffer (and ,bind-current-buffer (current-buffer))))
     (deferred:$
       ,request
       (deferred:nextc it
         (lambda (response)
           (cl-macrolet ((with-optional-current-buffer
                          (buffer-or-name &rest body-2)
                          `(if ,buffer-or-name
                               (with-current-buffer ,buffer-or-name
                                 ,@body-2)
                             ,@body-2)))
             (with-optional-current-buffer
              request-buffer
              (if (ycmd--exception-p response)
                  (let-alist response
                    (if (string= .exception.TYPE "UnknownExtraConf")
                        (ycmd--handle-extra-conf-exception
                         .exception.extra_conf_file)
                      (unless ,dont-show-exception-msg
                        (message "%s: %s" .exception.TYPE .message))
                      ,on-exception-form
                      ,return-form))
                (if (null ',body) response ,@body)))))))))

(defun ycmd--on-visit-buffer ()
  "If `ycmd--buffer-visit-flag' is nil send BufferVisit event."
  (when (and (not ycmd--buffer-visit-flag)
             (ycmd--server-alive-p))
    (ycmd-with-handled-server-exceptions
        (ycmd--event-notification "BufferVisit")
      :bind-current-buffer t
      (setq ycmd--buffer-visit-flag t))))

(defun ycmd--on-close-buffer ()
  "Notify server that the current buffer is no longer open.
Cleanup emacs-ycmd variables."
  (when (ycmd--server-alive-p)
    (ycmd-with-handled-server-exceptions
        (ycmd--event-notification "BufferUnload")))
  (ycmd--teardown))

(defun ycmd--reset-parse-status ()
  (ycmd--report-status 'unparsed)
  (setq ycmd--last-modified-tick nil))

(defun ycmd--teardown ()
  "Teardown ycmd in current buffer."
  (ycmd--kill-timer ycmd--notification-timer)
  (ycmd--reset-parse-status)
  (setq ycmd--deferred-parse nil)
  (run-hooks 'ycmd-after-teardown-hook))

(defun ycmd--global-teardown ()
  "Teardown ycmd in all buffers."
  (ycmd--kill-timer ycmd--on-focus-timer)
  (setq ycmd--mode-keywords-loaded nil)
  (clrhash ycmd--available-completers)
  (ycmd--with-all-ycmd-buffers
    (ycmd--teardown)
    (setq ycmd--buffer-visit-flag nil)))

(defun ycmd-file-types-with-diagnostics (mode)
  "Find the ycmd file types for MODE which support semantic diagnostics.

Returns a possibly empty list of ycmd file type strings.  If this
is empty, then ycmd doesn't support semantic completion (or
diagnostics) for MODE."
  (-intersection
   ycmd--file-types-with-diagnostics
   (ycmd-major-mode-to-file-types mode)))

(defun ycmd-major-modes-with-diagnostics ()
  "Return a list with major-modes which support semantic diagnostics."
  (->>
   (--filter (member (cadr it) ycmd--file-types-with-diagnostics)
             ycmd-file-type-map)
   (--map (car it))))

(defun ycmd-deferred:sync! (d)
  "Wait for the given deferred task.
Error is raised if it is not processed within deferred chain D.
This is a slightly modified version of the original
`deferred:sync!' function, with using `accept-process-output'
wrapped with `with-current-buffer' for waiting instead of a
combination of `sit-for' and `sleep-for' and with a shorter wait
time."
  (progn
    (let ((last-value 'deferred:undefined*)
          uncaught-error)
      (deferred:try
        (deferred:nextc d
          (lambda (x) (setq last-value x)))
        :catch
        (lambda (err) (setq uncaught-error err)))
      (with-local-quit
        (while (and (eq 'deferred:undefined* last-value)
                    (not uncaught-error))
          (accept-process-output nil 0.01))
        (when uncaught-error
          (deferred:resignal uncaught-error))
        last-value))))

(defmacro ycmd-deferred:timeout (timeout-sec d)
  "Time out macro on a deferred task.
If the deferred task does not complete within TIMEOUT-SEC, this
macro cancels the deferred task D and returns nil. This is a
slightly modified version of the original `deferred:timeout'
macro, which takes the timeout var in seconds and the timeout
form returns the symbol `timeout'."
  (declare (indent 1) (debug t))
  `(deferred:earlier
     (deferred:nextc (deferred:wait (* ,timeout-sec 1000))
       (lambda () 'timeout))
     ,d))

(defun ycmd-open ()
  "Start a new ycmd server.

This kills any ycmd server already running (under ycmd.el's
control.)."
  (interactive)
  (ycmd-close)
  (ycmd--start-server)
  (ycmd--start-server-timeout-timer)
  (ycmd--start-keepalive-timer))

(defun ycmd-close (&optional status)
  "Shutdown any running ycmd server.
STATUS is a status symbol for `ycmd--report-status',
defaulting to `stopped'."
  (interactive)
  (ycmd--stop-server)
  (ycmd--global-teardown)
  (ycmd--kill-timer ycmd--keepalive-timer)
  (ycmd--kill-timer ycmd--server-timeout-timer)
  (ycmd--with-all-ycmd-buffers
    (ycmd--report-status (or status 'stopped))))

(defun ycmd--stop-server ()
  "Stop the ycmd server process.

Send a `shutdown' request to the ycmd server and wait for the
ycmd server to stop.  If the ycmd server is still running after a
timeout specified by `ycmd-delete-process-delay', then kill the
process with `delete-process'."
  (when (ycmd--server-alive-p)
    (let ((start-time (float-time)))
      (ycmd-deferred:sync!
       (ycmd-deferred:timeout 0.1
         (ycmd-with-handled-server-exceptions
             (ycmd--request (make-ycmd-request-data
                             :handler "shutdown" :content nil))
           :dont-show-exception-msg t)))
      (while (and (ycmd-running-p)
                  (> ycmd-delete-process-delay
                     (- (float-time) start-time)))
        (sit-for 0.05))))
  (ignore-errors
    (delete-process ycmd--server-process-name)))

(defun ycmd-running-p ()
  "Return t if a ycmd server is already running."
  (--when-let (get-process ycmd--server-process-name)
    (and (processp it) (process-live-p it) t)))

(defun ycmd--server-alive-p ()
  "Return t if server is running and ready for requests."
  (and (ycmd-running-p) ycmd--server-actual-port))

(defmacro ycmd--ignore-errors (&rest body)
  "Execute BODY and ignore errors and request errors."
  `(let ((request-message-level -1)
         (request-log-level -1))
     (ignore-errors
       ,@body)))

(defun ycmd--keepalive ()
  "Sends an unspecified message to the server.

This is simply for keepalive functionality."
  (ycmd--ignore-errors
   (ycmd-with-handled-server-exceptions
       (ycmd--request (make-ycmd-request-data
                       :handler "healthy" :content nil)
                      :type "GET")
     :dont-show-exception-msg t)))

(defun ycmd--server-ready-p (&optional include-subserver)
  "Send request for server ready state.

If INCLUDE-SUBSERVER is non-nil, also request ready state for
semantic subserver."
  (when (ycmd--server-alive-p)
    (let* ((file-type
            (and include-subserver
                 (car-safe (ycmd-major-mode-to-file-types
                            major-mode))))
           (params (and file-type
                        (list (cons "subserver" file-type)))))
      (ycmd--ignore-errors
       (eq (ycmd-deferred:sync!
            (ycmd-with-handled-server-exceptions
                (ycmd--request
                 (make-ycmd-request-data :handler "ready" :content nil)
                 :params params :type "GET")
              :dont-show-exception-msg t))
           t)))))

(defun ycmd--extra-conf-request (filename &optional ignore-p)
  "Send extra conf request.
FILENAME is the path to a ycm_extra_conf file. If optional
IGNORE-P is non-nil ignore the ycm_extra_conf."
  (let ((handler (if ignore-p
                     "ignore_extra_conf_file"
                   "load_extra_conf_file"))
        (content (list (cons "filepath" filename))))
    (ycmd-deferred:sync!
     (ycmd-with-handled-server-exceptions
         (ycmd--request (make-ycmd-request-data
                         :handler handler :content content))))))

(defun ycmd-load-conf-file (filename)
  "Tell the ycmd server to load the configuration file FILENAME."
  (interactive
   (list
    (read-file-name "Filename: ")))
  (let ((filename (expand-file-name filename)))
    (ycmd--extra-conf-request filename)))

(defun ycmd-display-completions ()
  "Get completions at the current point and display them in a buffer.

This is really a utility/debugging function for developers, but
it might be interesting for some users."
  (interactive)
  (ycmd-with-handled-server-exceptions (ycmd-get-completions)
    (if (not response)
        (message "No completions available")
      (pop-to-buffer "*ycmd-completions*")
      (erase-buffer)
      (insert (pp-to-string response)))))

(defun ycmd-complete (&optional _ignored)
  "Completion candidates at point."
  (-when-let* ((completions (ycmd-deferred:sync!
                             (ycmd-deferred:timeout 0.5
                               (ycmd-with-handled-server-exceptions
                                   (ycmd-get-completions)))))
               (candidates (cdr (assq 'completions completions))))
    (--map (let* ((text (cdr (assq 'insertion_text it)))
                  (anno (cdr (assq 'menu_text it))))
             (when (and anno (string-match (regexp-quote text) anno))
               (put-text-property
                0 1 'anno (substring anno (match-end 0)) text))
             text)
           candidates)))

(defun ycmd-complete-at-point ()
  "Complete symbol at point."
  (unless (nth 3 (syntax-ppss)) ;; not in string
    (let* ((bounds (bounds-of-thing-at-point 'symbol))
           (beg (or (car bounds) (point)))
           (end (or (cdr bounds) (point))))
      (list beg end
            (completion-table-dynamic #'ycmd-complete)
            :annotation-function
            (lambda (arg) (get-text-property 0 'anno arg))))))

(defun ycmd-toggle-force-semantic-completion ()
  "Toggle whether to use always semantic completion.

Returns the new value of `ycmd-force-semantic-completion'."
  (interactive)
  (let ((force (not ycmd-force-semantic-completion)))
    (message "ycmd: force semantic completion %s."
             (if force "enabled" "disabled"))
    (setq ycmd-force-semantic-completion force)))

(defun ycmd--string-list-p (obj)
  "Return t if OBJ is a list of strings."
  (and (listp obj) (-all? #'stringp obj)))

(defun ycmd--locate-default-tags-file (buffer)
  "Look up directory hierarchy for first found default tags file for BUFFER."
  (-when-let* ((file (buffer-file-name buffer))
               (dir (and file
                         (locate-dominating-file
                          file ycmd-default-tags-file-name))))
    (expand-file-name ycmd-default-tags-file-name dir)))

(defun ycmd--get-tag-files (buffer)
  "Get tag files list for current BUFFER or nil."
  (--when-let (cond ((eq ycmd-tag-files 'auto)
                     (ycmd--locate-default-tags-file buffer))
                    ((or (stringp ycmd-tag-files)
                         (ycmd--string-list-p ycmd-tag-files))
                     ycmd-tag-files))
    (unless (listp it)
      (setq it (list it)))
    (mapcar 'expand-file-name it)))

(defun ycmd--get-keywords (buffer)
  "Get syntax keywords for BUFFER."
  (with-current-buffer buffer
    (let ((mode major-mode))
      (unless (memq mode ycmd--mode-keywords-loaded)
        (--when-let (and (functionp ycmd-get-keywords-function)
                         (funcall ycmd-get-keywords-function mode))
          (when (ycmd--string-list-p it)
            (add-to-list 'ycmd--mode-keywords-loaded mode)
            it))))))

(defun ycmd--get-keywords-from-alist (mode)
  "Get keywords from `ycmd-keywords-alist' for MODE."
  (let ((symbols (cdr (assq mode ycmd-keywords-alist))))
    (if (consp symbols)
        symbols
      (cdr (assq symbols ycmd-keywords-alist)))))

(defun ycmd-get-completions ()
  "Get completions in current buffer from the ycmd server.

Returns a deferred object which yields the HTTP message
content.  If completions are available, the structure looks like
this:

   ((error)
    (completion_start_column . 6)
    (completions
     ((kind . \"FUNCTION\")
      (extra_menu_info . \"long double\")
      (detailed_info . \"long double acoshl( long double )\\n\")
      (insertion_text . \"acoshl\")
      (menu_text . \"acoshl( long double )\"))
      . . .))

If ycmd can't do completion because it's busy parsing, the
structure looks like this:

  ((message . \"Still parsing file, no completions yet.\")
   (traceback . \"long traceback string\")
   (exception
    (TYPE . \"RuntimeError\")))

To see what the returned structure looks like, you can use
`ycmd-display-completions'."
  (let ((extra-data (and ycmd-force-semantic-completion
                         (list (cons "force_semantic" t)))))
    (ycmd--request (make-ycmd-request-data
                    :handler "completions"
                    :content (append (ycmd--get-basic-request-data)
                                     extra-data)))))

(defun ycmd--command-request (subcommand)
  "Send a command request for SUBCOMMAND."
  (let* ((subcommand (if (listp subcommand)
                         subcommand
                       (list subcommand)))
         (content (cons (append (list "command_arguments")
                                subcommand)
                        (ycmd--get-basic-request-data))))
    (ycmd--request (make-ycmd-request-data
                    :handler "run_completer_command"
                    :content content))))

(defun ycmd--run-completer-command (subcommand success-handler)
  "Send SUBCOMMAND to the `ycmd' server.

SUCCESS-HANDLER is called when for a successful response."
  (declare (indent 1))
  (when ycmd-mode
    (let ((cmd (or (car-safe subcommand) subcommand)))
      (if (ycmd-parsing-in-progress-p)
          (message "Can't send \"%s\" request while parsing is in progress!" cmd)
        (let ((pos (point)))
          (ycmd-with-handled-server-exceptions (ycmd--command-request subcommand)
            :bind-current-buffer t
            :on-exception-form (run-hook-with-args 'ycmd-after-exception-hook
                                                   cmd request-buffer pos response)
            (when (and response success-handler)
              (funcall success-handler response))))))))

(defun ycmd--unsupported-subcommand-p (response)
  "Return t if RESPONSE is an unsupported subcommand exception."
  (let-alist response
    (and (string= "ValueError" .exception.TYPE)
         (or (string-prefix-p "Supported commands are:\n" .message)
             (string= "This Completer has no supported subcommands."
                      .message)))))

(defun ycmd--get-defined-subcommands ()
  "Get available subcommands for current completer.
This is a blocking request."
  (let* ((data (make-ycmd-request-data
                :handler "defined_subcommands")))
    (ycmd-deferred:sync!
     (ycmd-with-handled-server-exceptions (ycmd--request data)))))

(defun ycmd--get-prompt-for-subcommand (subcommand)
  "Return promp for SUBCOMMAND that requires arguments."
  (pcase subcommand
    (`"RefactorRename"
     "New name: ")
    ((pred (and "RestartServer" (eq major-mode 'python-mode)))
     "Python binary: ")))

(defun ycmd--read-subcommand ()
  "Read subcommand from minibuffer."
  (--when-let (ycmd--get-defined-subcommands)
    (funcall ycmd-completing-read-function
             "Subcommand: " it nil t)))

(defun ycmd-completer (subcommand)
  "Run SUBCOMMAND for current completer."
  (interactive
   (list (-when-let (cmd (ycmd--read-subcommand))
           (--when-let (ycmd--get-prompt-for-subcommand cmd)
             (let ((arg (read-string it)))
               (unless (s-blank-str? arg)
                 (setq cmd (list cmd arg)))))
           cmd)))
  (when subcommand
    (ycmd--run-completer-command subcommand
      (lambda (response)
        (cond ((not (listp response))
               ;; If not a list, the response is necessarily a scalar:
               ;; boolean, number, string, etc. In this case, we print it to
               ;; the user.
               (message "%s" response))
              ((assq 'fixits response)
               (ycmd--handle-fixit-response response))
              ((assq 'message response)
               (ycmd--handle-message-response response))
              ((assq 'detailed_info response)
               (ycmd--handle-detailed-info-response response))
              (t
               (ycmd--handle-goto-response response)))))))

(defun ycmd-goto ()
  "Go to the definition or declaration of the symbol at current position."
  (interactive)
  (ycmd--goto "GoTo"))

(defun ycmd-goto-declaration ()
  "Go to the declaration of the symbol at the current position."
  (interactive)
  (ycmd--goto "GoToDeclaration"))

(defun ycmd-goto-definition ()
  "Go to the definition of the symbol at the current position."
  (interactive)
  (ycmd--goto "GoToDefinition"))

(defun ycmd-goto-implementation ()
  "Go to the implementation of the symbol at the current position."
  (interactive)
  (ycmd--goto "GoToImplementation"))

(defun ycmd-goto-include ()
  "Go to the include of the symbol at the current position."
  (interactive)
  (ycmd--goto "GoToInclude"))

(defun ycmd-goto-imprecise ()
  "Fast implementation of Go To at the cost of precision.
Useful in case compile-time is considerable."
  (interactive)
  (ycmd--goto "GoToImprecise"))

(defun ycmd-goto-references ()
  "Get references."
  (interactive)
  (ycmd--goto "GoToReferences"))

(defun ycmd-goto-type ()
  "Go to the type of the symbol at the current position."
  (interactive)
  (ycmd--goto "GoToType"))

(defun ycmd--save-marker ()
  "Save marker."
  (push-mark)
  (if (fboundp 'xref-push-marker-stack)
      (xref-push-marker-stack)
    (with-no-warnings
      (ring-insert find-tag-marker-ring (point-marker)))))

(defun ycmd--location-data-p (response)
  "Return t if RESPONSE is a GoTo location."
  (and (assq 'filepath response)
       (assq 'line_num response)
       (assq 'column_num response)))

(defun ycmd--handle-goto-response (response)
  "Handle a successfull GoTo RESPONSE."
  (ycmd--save-marker)
  (if (ycmd--location-data-p response)
      (ycmd--goto-location response 'find-file)
    (ycmd--view response major-mode)))

(defun ycmd--goto (type)
  "Implementation of GoTo according to the request TYPE."
  (save-excursion
    (--when-let (bounds-of-thing-at-point 'symbol)
      (goto-char (car it)))
    (ycmd-completer type)))

(defun ycmd--goto-location (location find-function)
  "Move cursor to LOCATION with FIND-FUNCTION.

LOCATION is a structure as returned from e.g. the various GoTo
commands."
  (let-alist location
    (when .filepath
      (funcall find-function .filepath)
      (goto-char (ycmd--col-line-to-position
                  .column_num .line_num)))))

(defun ycmd--goto-line (line)
  "Go to LINE."
  (goto-char (point-min))
  (forward-line (1- line)))

(defun ycmd--col-line-to-position (col line &optional buffer)
  "Convert COL and LINE into a position in the current buffer.

COL and LINE are expected to be as returned from ycmd, e.g. from
notify-file-ready.  Apparently COL can be 0 sometimes, in which
case this function returns 0.
Use BUFFER if non-nil or `current-buffer'."
  (let ((buff (or buffer (current-buffer))))
    (if (= col 0)
        0
      (with-current-buffer buff
        (ycmd--goto-line line)
        (forward-char (- col 1))
        (point)))))

(defun ycmd-clear-compilation-flag-cache ()
  "Clear the compilation flags cache."
  (interactive)
  (ycmd-completer "ClearCompilationFlagCache"))

(defun ycmd-restart-semantic-server (&optional arg)
  "Send request to restart the semantic completion backend server.
If ARG is non-nil and current `major-mode' is `python-mode',
prompt for the Python binary."
  (interactive
   (list (and current-prefix-arg
              (eq major-mode 'python-mode)
              (read-string "Python binary: "))))
  (let ((subcommand "RestartServer"))
    (unless (s-blank-str? arg)
      (setq subcommand (list subcommand arg)))
    (ycmd-completer subcommand)))

(cl-defun ycmd--fontify-code (code &optional (mode major-mode))
  "Fontify CODE."
  (cl-check-type mode function)
  (if (not (stringp code))
      code
    (with-temp-buffer
      (delay-mode-hooks (funcall mode))
      (setq font-lock-mode t)
      (funcall font-lock-function font-lock-mode)
      (let ((inhibit-read-only t))
        (erase-buffer)
        (insert code)
        (font-lock-default-fontify-region
         (point-min) (point-max) nil))
      (buffer-string))))

(defun ycmd--get-message (response)
  "Extract message from RESPONSE.
Return a cons cell with the type or parent as car. If cdr is
non-nil, the result is a valid type or parent."
  (--when-let (cdr (assq 'message response))
    (pcase it
      ((or `"Unknown semantic parent"
           `"Unknown type"
           `"Internal error: cursor not valid"
           `"Internal error: no translation unit")
       (cons it nil))
      (_ (cons it t)))))

(defun ycmd--handle-message-response (response)
  "Handle a successful GetParent or GetType RESPONSE."
  (--when-let (ycmd--get-message response)
    (pcase-let ((`(,msg . ,is-type-p) it))
      (message "%s" (if is-type-p
                        (ycmd--fontify-code msg)
                      msg)))))

(defun ycmd-get-parent ()
  "Get semantic parent for symbol at point."
  (interactive)
  (ycmd-completer "GetParent"))

(defun ycmd-get-type (&optional arg)
  "Get type for symbol at point.
If optional ARG is non-nil, get type without reparsing buffer."
  (interactive "P")
  (ycmd-completer (if arg "GetTypeImprecise" "GetType")))

;;; FixIts

(defmacro ycmd--loop-chunks-by-filename (spec &rest body)
  "Loop over an alist of fixit chunks grouped by filepath.
Evaluate BODY with `it' bound to each car from FIXIT-CHUNKS, in
turn. The structure of `it' is a cons cell (FILEPATH CHUNK-LIST).
Then evaluate RESULT to get return value, default nil.

\(fn (FIXIT-CHUNKS [RESULT]) BODY...)"
  (declare (indent 1) (debug ((form &optional form) body)))
  `(let ((chunks-by-filepath
          (--group-by (let-alist it .range.start.filepath)
                      ,(car spec))))
     (dolist (it chunks-by-filepath)
       ,@body)
     ,@(cdr spec)))

(defun ycmd--show-fixits (fixits &optional title)
  "Select a buffer and display FIXITS.
Optional TITLE is shown on first line."
  (let ((fixits-buffer (get-buffer-create "*ycmd-fixits*"))
        (fixit-num 1))
    (with-current-buffer fixits-buffer
      (setq buffer-read-only nil)
      (erase-buffer)
      (when title (insert (propertize title 'face 'bold)))
      (dolist (fixit fixits)
        (let-alist fixit
          (let* ((diffs (ycmd--get-fixit-diffs .chunks))
                 (multiple-fixits-p (> (length diffs) 1))
                 button-diff)
            (dolist (diff diffs)
              (pcase-let ((`(,diff-text . ,diff-path) diff))
                (setq button-diff
                      (concat button-diff (when (or (s-blank-str? .text)
                                                    multiple-fixits-p)
                                            (format "%s\n" diff-path))
                              (ycmd--fontify-code diff-text 'diff-mode) "\n"))))
            (ycmd--insert-fixit-button
             (concat (format "%d: %s\n" fixit-num .text) button-diff) .chunks)
            (cl-incf fixit-num))))
      (goto-char (point-min))
      (when title (forward-line 1))
      (ycmd-fixit-mode))
    (pop-to-buffer fixits-buffer)
    (setq next-error-last-buffer fixits-buffer)))

(defun ycmd--get-fixit-diffs (chunks)
  "Return a list of diffs for CHUNKS.
Each diff is a list of the actual diff, the path of the file for
the diff and a flag whether to show the filepath as part of the
button text. The flag is set to t when there are multiple diff
chunks for the file."
  (let (diffs)
    (ycmd--loop-chunks-by-filename (chunks (nreverse diffs))
      (pcase-let* ((`(,filepath . ,chunk) it)
                   (buffer (find-file-noselect filepath))
                   (buffertext (with-current-buffer buffer (buffer-string)))
                   (diff-buffer (with-temp-buffer
                                  (insert buffertext)
                                  (ycmd--replace-chunk-list chunk (current-buffer))
                                  (diff-no-select buffer (current-buffer)
                                                  "-U0 --strip-trailing-cr" t))))
        (with-current-buffer diff-buffer
          (goto-char (point-min))
          (unless (eobp)
            (ignore-errors
              (diff-beginning-of-hunk t))
            (while (looking-at diff-hunk-header-re-unified)
              (let* ((beg (point))
                     (end (diff-end-of-hunk))
                     (diff (buffer-substring-no-properties beg end)))
                (push (cons diff filepath) diffs)))))))))

(define-button-type 'ycmd--fixit-button
  'action #'ycmd--apply-fixit
  'face nil)

(defun ycmd--insert-fixit-button (name fixit)
  "Insert a button with NAME and FIXIT."
  (insert-text-button
   name
   'type 'ycmd--fixit-button
   'fixit fixit))

(defun ycmd--apply-fixit (button)
  "Apply BUTTON's FixIt chunk."
  (-when-let (chunks (button-get button 'fixit))
    (ycmd--loop-chunks-by-filename (chunks)
      (ycmd--replace-chunk-list (cdr it)))
    (quit-window t (get-buffer-window "*ycmd-fixits*"))))

(define-derived-mode ycmd-fixit-mode ycmd-view-mode "ycmd-fixits"
  "Major mode for viewing and navigation of fixits.

\\{ycmd-view-mode-map}"
  (local-set-key (kbd "q") (lambda () (interactive) (quit-window t))))

(defun ycmd--replace-chunk (bounds replacement-text line-delta char-delta buffer)
  "Replace text between BOUNDS with REPLACEMENT-TEXT.

BOUNDS is a list of two cons cells representing the start and end
of a chunk with a line and column pair (car and cdr). LINE-DELTA
and CHAR-DELTA are offset from -former replacements on the
current line. BUFFER is the current working buffer."
  (pcase-let* ((`((,start-line . ,start-column) (,end-line . ,end-column)) bounds)
               (start-line (+ start-line line-delta))
               (end-line (+ end-line line-delta))
               (source-line-count (1+ (- end-line start-line)))
               (start-column (+ start-column char-delta))
               (end-column (if (= source-line-count 1)
                               (+ end-column char-delta)
                             end-column))
               (replacement-lines (s-split "\n" replacement-text))
               (replacement-lines-count (length replacement-lines))
               (new-line-delta (- replacement-lines-count source-line-count))
               (new-char-delta (- (length (car (last replacement-lines)))
                                  (- end-column start-column))))
    (when (> replacement-lines-count 1)
      (setq new-char-delta (- new-char-delta start-column)))
    (save-excursion
      (with-current-buffer buffer
        (delete-region
         (ycmd--col-line-to-position start-column start-line buffer)
         (ycmd--col-line-to-position end-column end-line buffer))
        (insert replacement-text)
        (cons new-line-delta new-char-delta)))))

(defun ycmd--get-chunk-bounds (chunk)
  "Get an list with bounds of CHUNK."
  (let-alist chunk
    (list (cons .range.start.line_num .range.start.column_num)
          (cons .range.end.line_num .range.end.column_num))))

(defun ycmd--chunk-< (c1 c2)
  "Return t if C1 should go before C2."
  (pcase-let ((`((,line-num-1 . ,column-num-1) ,_) (ycmd--get-chunk-bounds c1))
              (`((,line-num-2 . ,column-num-2) ,_) (ycmd--get-chunk-bounds c2)))
    (or (< line-num-1 line-num-2)
        (and (= line-num-1 line-num-2)
             (< column-num-1 column-num-2)))))

(defun ycmd--replace-chunk-list (chunks &optional buffer)
  "Replace list of CHUNKS.

If BUFFER is specified use it as working buffer, else use buffer
specified in fixit chunk."
  (let ((chunks-sorted (sort chunks 'ycmd--chunk-<))
        (last-line -1)
        (line-delta 0)
        (char-delta 0))
    (dolist (c chunks-sorted)
      (let-alist c
        (pcase-let* ((chunk-bounds (ycmd--get-chunk-bounds c))
                     (`((,start-line . ,_) (,end-line . ,_)) chunk-bounds)
                     (buffer (or buffer (find-file-noselect
                                         .range.start.filepath))))
          (unless (= start-line last-line)
            (setq last-line end-line)
            (setq char-delta 0))
          (pcase-let ((`(,new-line-delta . ,new-char-delta)
                       (ycmd--replace-chunk chunk-bounds .replacement_text
                                            line-delta char-delta buffer)))
            (setq line-delta (+ line-delta new-line-delta))
            (setq char-delta (+ char-delta new-char-delta))))))))

(defun ycmd--fixits-have-same-location-p (fixits)
  "Check if mutiple FIXITS have the same location."
  (let ((fixits-by-location
         (--group-by (cdr (assq 'location it)) fixits)))
    (catch 'done
      (dolist (f fixits-by-location)
        (when (> (length (cdr f)) 1)
          (throw 'done t))))))

(defun ycmd--handle-fixit-response (response)
  "Handle a fixit RESPONSE."
  (let ((fixits (cdr (assq 'fixits response))))
    (if (not fixits)
        (message "No fixits found for current line")
      (let ((multiple-fixits-p
             (and (> (length fixits) 1)
                  (ycmd--fixits-have-same-location-p fixits))))
        (if (and (not ycmd-confirm-fixit) (not multiple-fixits-p))
            (let ((num-changes-applied 0)
                  files-changed)
              (dolist (fixit fixits)
                (-when-let (chunks (cdr (assq 'chunks fixit)))
                  (ycmd--loop-chunks-by-filename (chunks)
                    (pcase-let ((`(,chunk-path . ,chunk) it))
                      (ycmd--replace-chunk-list chunk)
                      (cl-incf num-changes-applied (length chunk))
                      (unless (member chunk-path files-changed)
                        (setq files-changed (append (list chunk-path)
                                                    files-changed)))))))
              (when (> num-changes-applied 0)
                (let* ((num-files-changed (length files-changed))
                       (text
                        (concat (format "Applied %d changes" num-changes-applied)
                                (when (> num-files-changed 1)
                                  (format " in %d files" num-files-changed)))))
                  (message text))))
          (save-current-buffer
            (ycmd--show-fixits
             fixits (and multiple-fixits-p
                         (concat
                          "Multiple FixIt suggestions are available at this location."
                          "Which one would you like to apply?\n")))))))))

(defun ycmd-fixit()
  "Get FixIts for current line."
  (interactive)
  (ycmd-completer "FixIt"))

(defun ycmd-refactor-rename (new-name)
  "Refactor current context with NEW-NAME."
  (interactive "MNew variable name: ")
  (let ((subcommand "RefactorRename"))
    (unless (s-blank-str? new-name)
      (setq subcommand (list subcommand new-name)))
    (ycmd-completer subcommand)))

(defun ycmd-show-documentation (&optional arg)
  "Show documentation for current point in buffer.

If optional ARG is non-nil do not reparse buffer before getting
the documentation."
  (interactive "P")
  (ycmd-completer (if arg "GetDocImprecise" "GetDoc")))

(defun ycmd--handle-detailed-info-response (response)
  "Handle successful GetDoc RESPONSE."
  (let ((documentation (cdr (assq 'detailed_info response))))
    (if (not (s-blank? documentation))
        (with-help-window (get-buffer-create " *ycmd-documentation*")
          (with-current-buffer standard-output
            (insert documentation)))
      (message "No documentation available for current context"))))

(defmacro ycmd--with-view-buffer (&rest body)
  "Create view buffer and execute BODY in it."
  `(let ((buf (get-buffer-create "*ycmd-locations*")))
     (with-current-buffer buf
       (setq buffer-read-only nil)
       (erase-buffer)
       ,@body
       (goto-char (point-min))
       (ycmd-view-mode)
       buf)))

(defun ycmd--view (response mode)
  "Select `ycmd-view-mode' buffer and display items from RESPONSE.
MODE is a major mode for fontifaction."
  (let ((view-buffer
         (ycmd--with-view-buffer
          (->>
           (--group-by (cdr (assq 'filepath it)) response)
           (mapc (lambda (it) (ycmd--view-insert-location it mode)))))))
    (pop-to-buffer view-buffer)
    (setq next-error-last-buffer view-buffer)))

(define-button-type 'ycmd--location-button
  'action #'ycmd--view-jump
  'face nil)

(defun ycmd--view-jump (button)
  "Jump to BUTTON's location in current window."
  (let ((location (button-get button 'location)))
    (ycmd--goto-location location 'find-file)))

(defun ycmd--view-jump-other-window (button)
  "Jump to BUTTON's location in other window."
  (let ((location (button-get button 'location)))
    (ycmd--goto-location location 'find-file-other-window)))

(defun ycmd--view-insert-button (name location)
  "Insert a view button with NAME and LOCATION."
  (insert-text-button
   name
   'type 'ycmd--location-button
   'location location))

(defun ycmd--get-line-from-location (location)
  "Return line from LOCATION."
  (let-alist location
    (--when-let (and .filepath (find-file-noselect .filepath))
      (with-current-buffer it
        (goto-char (ycmd--col-line-to-position
                    .column_num .line_num))
        (back-to-indentation)
        (buffer-substring (point) (line-end-position))))))

(defun ycmd--view-insert-location (location-group mode)
  "Insert LOCATION-GROUP into `current-buffer' and fontify according MODE.
LOCATION-GROUP is a cons cell whose car is the filepath and the whose
cdr is a list of location objects."
  (pcase-let* ((`(,filepath . ,locations) location-group)
               (max-line-num-width
                (cl-loop for location in locations
                         maximize (let ((line-num (cdr (assq 'line_num location))))
                                    (and line-num (length (format "%d" line-num))))))
               (line-num-format (and max-line-num-width
                                     (format "%%%dd:" max-line-num-width))))
    (insert (propertize (concat filepath "\n") 'face 'bold))
    (mapc (lambda (it)
            (let-alist it
              (when line-num-format
                (insert (format line-num-format .line_num)))
              (insert "    ")
              (let ((description (or (and (not (s-blank? .description))
                                          (s-trim-left .description))
                                     (ycmd--get-line-from-location it))))
                (ycmd--view-insert-button
                 (ycmd--fontify-code (or description "") mode) it))
              (insert "\n")))
          locations)))

(defvar ycmd-view-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "n") 'next-error-no-select)
    (define-key map (kbd "p") 'previous-error-no-select)
    (define-key map (kbd "q") 'quit-window)
    map))

(define-derived-mode ycmd-view-mode special-mode "ycmd-view"
  "Major mode for locations view and navigation for `ycmd-mode'.

\\{ycmd-view-mode-map}"
  (setq next-error-function #'ycmd--next-location))

(defun ycmd--next-location (num _reset)
  "Navigate to the next location in the view buffer.
NUM is the number of locations to move forward.  If RESET is
non-nil got to the beginning of buffer before locations
navigation."
  (forward-button num)
  (ycmd--view-jump-other-window (button-at (point))))

(define-button-type 'ycmd--error-button
  'face '(error bold underline)
  'button 't)

(define-button-type 'ycmd--warning-button
  'face '(warning bold underline)
  'button 't)

(defun ycmd--make-button (start end type msg)
  "Make a button from START to END of TYPE in the current buffer.

When clicked, MSG will be shown in the minibuffer."
  (make-text-button
   start end
   'type type
   'action (lambda (_) (message msg))))

(defconst ycmd--file-ready-buttons
  '(("ERROR" . ycmd--error-button)
    ("WARNING" . ycmd--warning-button))
  "A mapping from parse 'kind' to button types.")

(defun ycmd--line-start-position (line)
  "Find position at the start of LINE."
  (save-excursion
    (ycmd--goto-line line)
    (beginning-of-line)
    (point)))

(defun ycmd--line-end-position (line)
  "Find position at the end of LINE."
  (save-excursion
    (ycmd--goto-line line)
    (end-of-line)
    (point)))

(defun ycmd--decorate-single-parse-result (result)
  "Decorates a buffer based on the contents of a single parse RESULT.

This is a fairly crude form of decoration, but it does give
reasonable visual feedback on the problems found by ycmd."
  (let-alist result
    (--when-let (find-buffer-visiting .location.filepath)
      (with-current-buffer it
        (let* ((start-pos (ycmd--line-start-position .location.line_num))
               (end-pos (ycmd--line-end-position .location.line_num))
               (btype (cdr (assoc .kind ycmd--file-ready-buttons))))
          (when btype
            (with-silent-modifications
              (ycmd--make-button
               start-pos end-pos
               btype (concat .kind ": " .text
                             (when (eq .fixit_available t)
                               " (FixIt available)"))))))))))

(defun ycmd-decorate-with-parse-results (results)
  "Decorates a buffer using the RESULTS of a file-ready parse list.

This is suitable as an entry in `ycmd-file-parse-result-hook'."
  (with-silent-modifications
    (set-text-properties (point-min) (point-max) nil))
  (mapc 'ycmd--decorate-single-parse-result results)
  results)

(defun ycmd--display-single-file-parse-result (result)
  "Insert a single file parse RESULT."
  (let-alist result
    (insert (format "%s:%s - %s - %s\n"
                    .location.filepath .location.line_num
                    .kind .text))))

(defun ycmd-display-file-parse-results (results)
  "Display parse RESULTS in a buffer."
  (let ((buffer "*ycmd-file-parse-results*"))
    (get-buffer-create buffer)
    (with-current-buffer buffer
      (erase-buffer)
      (mapc 'ycmd--display-single-file-parse-result results))
    (display-buffer buffer)))

(defun ycmd-parse-buffer ()
  "Parse buffer."
  (interactive)
  (if (not (ycmd-semantic-completer-available-p))
      (message "Native filetype completion not supported for current file, \
cannot send parse request")
    (when (ycmd--server-alive-p)
      (let ((buffer (current-buffer)))
        (deferred:$
          (deferred:next
            (lambda ()
              (message "Parsing buffer...")
              (ycmd--reset-parse-status)
              (ycmd--conditional-parse)))
          (deferred:nextc it
            (lambda ()
              (with-current-buffer buffer
                (when (eq ycmd--last-status-change 'parsed)
                  (message "Parsing buffer done"))))))))))

(defun ycmd--handle-extra-conf-exception (conf-file)
  "Handle an exception of type `UnknownExtraConf'.

Handle CONF-FILE according the value of `ycmd-extra-conf-handler'."
  (if (not conf-file)
      (warn "No extra_conf_file included in UnknownExtraConf exception. \
Consider reporting this.")
    (let ((ignore-p (or (eq ycmd-extra-conf-handler 'ignore)
                        (not (y-or-n-p (format "Load YCMD extra conf %s? "
                                               conf-file))))))
      (ycmd--extra-conf-request conf-file ignore-p)
      (ycmd--reset-parse-status)
      (ycmd-notify-file-ready-to-parse))))

(defun ycmd--event-notification (event-name &optional extra-data)
  "Send a event notification for EVENT-NAME.
Optional EXTRA-DATA contains additional data for the request."
  (let ((content (append (list (cons "event_name" event-name))
                         (ycmd--get-basic-request-data)
                         extra-data)))
    (deferred:try
      (ycmd--request (make-ycmd-request-data
                      :handler "event_notification"
                      :content content))
      :catch
      (lambda (err)
        (message "Error sending %s request: %s" event-name err)
        (ycmd--report-status 'errored)
        nil))))

(defun ycmd-notify-file-ready-to-parse ()
  "Send a notification to ycmd that the buffer is ready to be parsed.

Only one active notification is allowed per buffer, and this
function enforces that constraint.

The response of the notification are passed to all of the
functions in `ycmd-file-parse-result-hook'."
  (when (and ycmd-mode (not (ycmd-parsing-in-progress-p)))
    (ycmd-with-handled-server-exceptions
        (let ((extra-data
               (append (--when-let (and ycmd-tag-files
                                        (ycmd--get-tag-files request-buffer))
                         (list (cons "tag_files" it)))
                       (--when-let (and ycmd-seed-identifiers-with-keywords
                                        (ycmd--get-keywords request-buffer))
                         (list (cons "syntax_keywords" it))))))
          (ycmd--report-status 'parsing)
          (ycmd--event-notification "FileReadyToParse" extra-data))
      :bind-current-buffer t
      :on-exception-form (ycmd--report-status 'errored)
      (ycmd--report-status 'parsed)
      (run-hook-with-args 'ycmd-file-parse-result-hook response))))

(defun ycmd-major-mode-to-file-types (mode)
  "Map a major mode MODE to a list of file-types suitable for ycmd.

If there is no established mapping, return nil."
  (cdr (assq mode ycmd-file-type-map)))

(defun ycmd--on-server-timeout ()
  "Kill server process due to timeout."
  (ycmd-close 'errored)
  (message "ERROR: Ycmd server timeout. If this happens regularly you may need to increase `ycmd-startup-timeout'."))

(defun ycmd--start-server-timeout-timer ()
  "Start the server timeout timer."
  (ycmd--kill-timer ycmd--server-timeout-timer)
  (setq ycmd--server-timeout-timer
        (run-with-timer
         ycmd-startup-timeout nil
         #'ycmd--on-server-timeout)))

(defun ycmd--start-keepalive-timer ()
  "Kill any existing keepalive timer and start a new one."
  (ycmd--kill-timer ycmd--keepalive-timer)
  (setq ycmd--keepalive-timer
        (run-with-timer
         ycmd-keepalive-period
         ycmd-keepalive-period
         #'ycmd--keepalive)))

(defun ycmd--generate-hmac-secret ()
  "Generate a new, random 16-byte HMAC secret key."
  (let ((result '()))
    (dotimes (_ 16 result)
      (setq result (cons (byte-to-string (random 256)) result)))
    (apply 'concat result)))

(defun ycmd--json-encode (obj)
  "Encode a json object OBJ.
A version of json-encode that uses {} instead of null for nil values.
This produces output for empty alists that ycmd expects."
  (cl-letf (((symbol-function 'json-encode-keyword)
             (lambda (k) (cond ((eq k t)          "true")
                               ((eq k json-false) "false")
                               ((eq k json-null)  "{}")))))
    (json-encode obj)))

;; This defines 'ycmd--hmac-function which we use to combine an HMAC
;; key and message contents.
(defun ycmd--secure-hash (x)
  "Generate secure sha256 hash of X."
  (secure-hash 'sha256 x nil nil 1))
(define-hmac-function ycmd--hmac-function
  ycmd--secure-hash 64 64)

(defun ycmd--options-contents (hmac-secret)
  "Return a struct with ycmd options and the HMAC-SECRET applied.
The struct can be json encoded into a file to create a ycmd
options file.

When we start a new ycmd server, it needs an options file.  It
reads this file and then deletes it since it contains a secret
key.  So we need to generate a new options file for each ycmd
instance.  This function effectively produces the contents of that
file."
  (let ((hmac-secret (base64-encode-string hmac-secret))
        (global-config (or ycmd-global-config ""))
        (extra-conf-whitelist (or ycmd-extra-conf-whitelist []))
        (confirm-extra-conf (if (eq ycmd-extra-conf-handler 'load) 0 1))
        (gocode-binary-path (or ycmd-gocode-binary-path ""))
        (godef-binary-path (or ycmd-godef-binary-path ""))
        (rust-src-path (or ycmd-rust-src-path ""))
        (swift-src-path (or ycmd-swift-src-path ""))
        (racerd-binary-path (or ycmd-racerd-binary-path ""))
        (python-binary-path (or ycmd-python-binary-path ""))
        (auto-trigger (if ycmd-auto-trigger-semantic-completion 1 0)))
    `((filepath_completion_use_working_dir . 0)
      (auto_trigger . ,auto-trigger)
      (min_num_of_chars_for_completion . ,ycmd-min-num-chars-for-completion)
      (min_num_identifier_candidate_chars . 0)
      (semantic_triggers . ())
      (filetype_specific_completion_to_disable (gitcommit . 1))
      (collect_identifiers_from_comments_and_strings . 0)
      (max_num_identifier_candidates . ,ycmd-max-num-identifier-candidates)
      (extra_conf_globlist . ,extra-conf-whitelist)
      (global_ycm_extra_conf . ,global-config)
      (confirm_extra_conf . ,confirm-extra-conf)
      (max_diagnostics_to_display . 30)
      (auto_start_csharp_server . 1)
      (auto_stop_csharp_server . 1)
      (use_ultisnips_completer . 1)
      (csharp_server_port . 0)
      (hmac_secret . ,hmac-secret)
      (server_keep_logfiles . 1)
      (gocode_binary_path . ,gocode-binary-path)
      (godef_binary_path . ,godef-binary-path)
      (rust_src_path . ,rust-src-path)
      (swift_src_path . ,swift-src-path)
      (racerd_binary_path . ,racerd-binary-path)
      (python_binary_path . ,python-binary-path))))

(defun ycmd--create-options-file (hmac-secret)
  "Create a new options file for a ycmd server with HMAC-SECRET.

This creates a new tempfile and fills it with options.  Returns
the name of the newly created file."
  (let ((options-file (make-temp-file "ycmd-options"))
        (options (ycmd--options-contents hmac-secret)))
    (with-temp-file options-file
      (insert (ycmd--json-encode options)))
    options-file))

(defun ycmd--exit-code-as-string (code)
  "Return exit status message for CODE."
  (pcase code
    (`3 "unexpected error while loading ycm_core.")
    (`4 (concat "ycm_core library not detected; "
                "you need to compile it by running the "
                "build.py script. See the documentation "
                "for more details."))
    (`5 (concat "ycm_core library compiled for Python 2 "
                "but loaded in Python 3."))
    (`6 (concat "ycm_core library compiled for Python 3 "
                "but loaded in Python 2."))
    (`7 (concat "ycm_core library too old; "
                "PLEASE RECOMPILE by running the build.py "
                "script. See the documentation for more details."))))

(defun ycmd--server-process-sentinel (process event)
  "Handle Ycmd server PROCESS EVENT."
  (when (memq (process-status process) '(exit signal))
    (let* ((code (process-exit-status process))
           (status (if (eq code 0) 'stopped 'errored)))
      (when (eq status 'errored)
        (--if-let (and (eq (process-status process) 'exit)
                       (ycmd--exit-code-as-string code))
            (message "Ycmd server error: %s" it)
          (message "Ycmd server %s" (s-replace "\n" "" event))))
      (ycmd--with-all-ycmd-buffers
        (ycmd--report-status status))
      (ycmd--kill-timer ycmd--keepalive-timer))))

(defun ycmd--server-process-filter (process string)
  "Filter function for the Ycmd server PROCESS output STRING."
  ;; insert string into process-buffer
  (when (buffer-live-p (process-buffer process))
    (with-current-buffer (process-buffer process)
      (let ((moving (= (point) (process-mark process))))
        (save-excursion
          (goto-char (process-mark process))
          (let ((inhibit-read-only t))
            (insert-before-markers string))
          (set-marker (process-mark process) (point)))
        (when moving (goto-char (process-mark process))))))
  ;; parse port from server output
  (when (and (not ycmd--server-actual-port)
             (string-match "^serving on http://.*:\\\([0-9]+\\\)$"
                           string))
    (ycmd--kill-timer ycmd--server-timeout-timer)
    (setq ycmd--server-actual-port
          (string-to-number (match-string 1 string)))
    (ycmd--with-all-ycmd-buffers
      (ycmd--reset-parse-status))
    (ycmd--perform-deferred-parse)))

(defun ycmd--get-process-environment ()
  "Return `process-evironment'.
If `ycmd-bypass-url-proxy-services' is non-nil, prepend
`no_proxy' variable to environment."
  (or ycmd--process-environment
      (setq ycmd--process-environment
            (append (and ycmd-bypass-url-proxy-services
                         (not (or (getenv "NO_PROXY")
                                  (getenv "no_PROXY")
                                  (getenv "no_proxy")))
                         (list (concat "NO_PROXY=" ycmd-host)))
                    process-environment))))

(defun ycmd--start-server ()
  "Start a new server and return the process."
  (unless ycmd-server-command
    (user-error "Error: The variable `ycmd-server-command' is not set.  \
See the docstring of the variable for an example"))
  (let ((proc-buff (get-buffer-create ycmd--server-buffer-name)))
    (with-current-buffer proc-buff
      (setq buffer-read-only t)
      (let ((inhibit-read-only t))
        (buffer-disable-undo)
        (erase-buffer)))
    (setq ycmd--process-environment nil)
    (let* ((port (and (numberp ycmd-server-port)
                      (> ycmd-server-port 0)
                      ycmd-server-port))
           (hmac-secret (ycmd--generate-hmac-secret))
           (options-file (ycmd--create-options-file hmac-secret))
           (args (append (and port (list (format "--port=%d" port)))
                         (list (concat "--options_file=" options-file))
                         ycmd-server-args))
           (server-program+args (append ycmd-server-command args))
           (process-environment (ycmd--get-process-environment))
           (proc (apply #'start-process ycmd--server-process-name proc-buff
                        server-program+args)))
      (ycmd--with-all-ycmd-buffers
        (ycmd--report-status 'starting))
      (setq ycmd--server-actual-port nil
            ycmd--hmac-secret hmac-secret)
      (set-process-query-on-exit-flag proc nil)
      (set-process-sentinel proc #'ycmd--server-process-sentinel)
      (set-process-filter proc #'ycmd--server-process-filter)
      proc)))

(defun ycmd-wait-until-server-is-ready (&optional include-subserver)
  "Wait until server is ready.
If INCLUDE-SUBSERVER is non-nil wait until subserver is ready.
Return t when server is ready.  Signal error in case of timeout.
The timeout can be set with the variable
`ycmd-startup-timeout'."
  (catch 'ready
    (let ((server-start-time (float-time)))
      (ycmd--kill-timer ycmd--server-timeout-timer)
      (while (ycmd-running-p)
        (sit-for 0.1)
        (if (ycmd--server-ready-p include-subserver)
            (progn
              (ycmd--with-all-ycmd-buffers
                (ycmd--reset-parse-status))
              (throw 'ready t))
          ;; timeout after specified period
          (when (< ycmd-startup-timeout
                   (- (float-time) server-start-time))
            (ycmd--on-server-timeout)))))))

(defun ycmd--column-in-bytes ()
  "Calculate column offset in bytes for the current position and buffer."
  (- (position-bytes (point))
     (position-bytes (line-beginning-position))))

;; https://github.com/abingham/emacs-ycmd/issues/165
(eval-and-compile
  (if (version-list-< (version-to-list emacs-version) '(25))
      (defun ycmd--encode-string (s) s)
    (defun ycmd--encode-string (s) (encode-coding-string s 'utf-8 t))))

(defun ycmd--get-basic-request-data ()
  "Build the basic request data alist for a server request."
  (let* ((column-num (+ 1 (ycmd--column-in-bytes)))
         (line-num (line-number-at-pos (point)))
         (full-path (ycmd--encode-string (or (buffer-file-name) "")))
         (file-contents (ycmd--encode-string
                         (buffer-substring-no-properties
                          (point-min) (point-max))))
         (file-types (or (ycmd-major-mode-to-file-types major-mode)
                         '("generic"))))
    `(("file_data" .
       ((,full-path . (("contents" . ,file-contents)
                       ("filetypes" . ,file-types)))))
      ("filepath" . ,full-path)
      ("line_num" . ,line-num)
      ("column_num" . ,column-num))))


(defvar ycmd--log-enabled nil
  "If non-nil, http content will be logged.
This is useful for debugging.")

(defun ycmd-toggle-log-enabled ()
  "Toggle `ycmd--log-enabled' variable."
  (interactive)
  (let ((log-enabled (not ycmd--log-enabled)))
    (message "Ycmd Log %s" (if log-enabled "enabled" "disabled"))
    (setq ycmd--log-enabled log-enabled)))

(defun ycmd--log-content (header content)
  "Insert log with HEADER and CONTENT in a buffer."
  (when ycmd--log-enabled
    (let ((buffer (get-buffer-create "*ycmd-content-log*")))
      (with-current-buffer buffer
        (save-excursion
          (goto-char (point-max))
          (insert (format "\n%s\n\n" header))
          (insert (pp-to-string content)))))))

(defun ycmd-show-debug-info ()
  "Show debug information."
  (interactive)
  (let ((data (make-ycmd-request-data :handler "debug_info"))
        (buffer (current-buffer)))
    (with-help-window (get-buffer-create " *ycmd-debug-info*")
      (with-current-buffer standard-output
        (princ "Ycmd debug information for buffer ")
        (insert (propertize (buffer-name buffer) 'face 'bold))
        (princ " in ")
        (let ((mode (buffer-local-value 'major-mode buffer)))
          (insert-button (symbol-name mode)
                         'type 'help-function
                         'help-args (list mode)))
        (princ ":\n\n")
        (--if-let (and (ycmd--server-alive-p)
                       (ycmd-deferred:sync!
                        (ycmd-with-handled-server-exceptions
                            (ycmd--request data))))
            (pp it)
          (princ "No debug info available from server"))
        (princ "\n\n")
        (princ "Server is ")
        (let ((running (ycmd--server-alive-p)))
          (insert (propertize (if running "running" "not running")
                              'face (if running 'success '(warning bold))))
          (when running
            (insert
             (format " at: %s:%d" ycmd-host ycmd--server-actual-port))))
        (princ "\n\n")
        (princ "Ycmd Mode is ")
        (let ((enabled (buffer-local-value 'ycmd-mode buffer)))
          (insert (propertize (if enabled "enabled" "disabled")
                              'face (if enabled 'success '(warning bold)))))
        (save-excursion
          (let ((end (point)))
            (backward-paragraph)
            (fill-region-as-paragraph (point) end)))

        (princ "\n\n--------------------\n\n")
        (princ (format "Ycmd version:   %s\n" (ignore-errors (ycmd-version))))
        (princ (format "Emacs version:  %s\n" emacs-version))
        (princ (format "System:         %s\n" system-configuration))
        (princ (format "Window system:  %S\n" window-system))))))

(defun ycmd-filter-and-sort-candidates (request-data)
  "Use ycmd to filter and sort identifiers from REQUEST-DATA.

This request allows to use ycmd's filtering and sorting
mechanism on arbitrary sets of identifiers.

The request data should be something like:

\((candidates \"candidate1\" \"candidate2\")
 (sort_property . \"\")
 (query . \"cand\"))

If candidates is a list with identifiers, sort_property should be
and empty string, however when candidates is a more complex
structure it is used to specify the sort key."
  (let ((data (make-ycmd-request-data
               :handler "filter_and_sort_candidates"
               :content request-data)))
    (ycmd-deferred:sync!
     (ycmd-with-handled-server-exceptions (ycmd--request data)))))

(defun ycmd--send-completer-available-request (&optional mode)
  "Send request to check if a semantic completer exists for MODE.
Response is non-nil if semantic complettion is available."
  (let ((data (make-ycmd-request-data
               :handler "semantic_completion_available")))
    (when mode
      (let* ((buffer (current-buffer))
             (full-path (ycmd--encode-string (or (buffer-file-name buffer) "")))
             (content (ycmd-request-data-content data))
             (file-types (assoc "filetypes"
                                (assoc full-path
                                       (assoc "file_data" content)))))
        (when (consp file-types)
          (setcdr file-types (ycmd-major-mode-to-file-types mode)))))
    (ycmd-deferred:sync!
     (ycmd-with-handled-server-exceptions (ycmd--request data)))))

(defun ycmd-semantic-completer-available-p ()
  "Return t if a semantic completer is available for current `major-mode'."
  (let ((mode major-mode))
    (or (gethash mode ycmd--available-completers)
        (--when-let (ycmd--send-completer-available-request mode)
          (puthash mode (or (eq it t) 'none) ycmd--available-completers)))))

(defun ycmd--get-request-hmac (method path body)
  "Generate HMAC for request from METHOD, PATH and BODY."
  (ycmd--hmac-function
   (mapconcat (lambda (val)
                (ycmd--hmac-function
                 (ycmd--encode-string val) ycmd--hmac-secret))
              `(,method ,path ,(or body "")) "")
   ycmd--hmac-secret))

(cl-defstruct ycmd-request-data
  "Structure for storing the ycmd server request data.

Slots:

`handler'
     Specifies the the path portion of the URL. For example, if
     HANDLER is 'feed_llama', the request URL is
     'http://host:port/feed_llama'.

`content'
     An alist that will be JSON-encoded and sent over at the
     content of the HTTP message."
  handler
  (content (ycmd--get-basic-request-data)))

(cl-defun ycmd--request (request-data
                         &key
                         (type "POST")
                         (params nil))
  "Send an asynchronous HTTP request to the ycmd server.

This starts the server if necessary.

Returns a deferred object which resolves to the content of the
response message.

REQUEST-DATA is a `ycmd-request-data' structure.

PARSER specifies the function that will be used to parse the
response to the message. Typical values are buffer-string and
json-read. This function will be passed an the completely
unmodified contents of the response (i.e. not JSON-decoded or
anything like that)."
  (unless (ycmd--server-alive-p)
    (message "Ycmd server is not running. Can't send `%s' request!"
             (ycmd-request-data-handler request-data))
    (cl-return-from ycmd--request (deferred:next)))

  (let* ((url-show-status (not ycmd-hide-url-status))
         (url-proxy-services (unless ycmd-bypass-url-proxy-services
                               url-proxy-services))
         (process-environment (ycmd--get-process-environment))
         (path (concat "/" (ycmd-request-data-handler request-data)))
         (content (json-encode (ycmd-request-data-content request-data)))
         (hmac (ycmd--get-request-hmac type path content))
         (encoded-hmac (base64-encode-string hmac 't))
         (url (format "http://%s:%s%s"
                      ycmd-host ycmd--server-actual-port path))
         (headers `(("Content-Type" . "application/json")
                    ("X-Ycm-Hmac" . ,encoded-hmac)))
         (parser (lambda ()
                   (let ((json-array-type 'list))
                     (json-read)))))
    (ycmd--log-content "HTTP REQUEST CONTENT" content)

    (deferred:$
      (request-deferred url :type type :params params :data content
                        :parser parser :headers headers)
      (deferred:nextc it
        (lambda (response)
          (let ((data (request-response-data response)))
            (ycmd--log-content "HTTP RESPONSE CONTENT" data)
            data))))))

(provide 'ycmd)

;;; ycmd.el ends here

;; Local Variables:
;; indent-tabs-mode: nil
;; End:
