;;; haskell-completions.el --- Haskell Completion package -*- lexical-binding: t -*-

;; Copyright © 2015-2016 Athur Fayzrakhmanov. All rights reserved.

;; This file is part of haskell-mode package.
;; You can contact with authors using GitHub issue tracker:
;; https://github.com/haskell/haskell-mode/issues

;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.

;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING.  If not, write to
;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.

;;; Commentary:

;; This package provides completions related functionality for
;; Haskell Mode such grab completion prefix at point, and etc..

;; Some description
;; ================
;;
;; For major use function `haskell-completions-grab-prefix' is supposed, and
;; other prefix grabbing functions are used internally by it.  So, only this
;; funciton have prefix minimal length functionality and invokes predicate
;; function `haskell-completions-can-grab-prefix'.

;;; Code:

(require 'haskell-mode)
(require 'haskell-process)
(require 'haskell-interactive-mode)

;;;###autoload
(defgroup haskell-completions nil
  "Settings for completions provided by `haskell-mode'"
  :link '(custom-manual "(haskell-mode)Completion support")
  :group 'haskell)

(defcustom haskell-completions-complete-operators
  t
  "Should `haskell-completions-sync-repl-completion-at-point' complete operators.

Note: GHCi prior to version 8.0.1 have bug in `:complete`
 command: when completing operators it returns a list of all
 imported identifiers (see Track ticket URL
 `https://ghc.haskell.org/trac/ghc/ticket/10576'). This leads to
 significant Emacs slowdown. To aviod slowdown you should set
 this variable to `nil'."
  :group 'haskell-completions
  :type 'boolean)

(defvar haskell-completions--pragma-names
  (list "DEPRECATED"
        "INCLUDE"
        "INCOHERENT"
        "INLINABLE"
        "INLINE"
        "LANGUAGE"
        "LINE"
        "MINIMAL"
        "NOINLINE"
        "NOUNPACK"
        "OPTIONS"
        "OPTIONS_GHC"
        "OVERLAPPABLE"
        "OVERLAPPING"
        "OVERLAPS"
        "RULES"
        "SOURCE"
        "SPECIALIZE"
        "UNPACK"
        "WARNING")
  "A list of supported pragmas.
This list comes from GHC documentation (URL
`https://downloads.haskell.org/~ghc/7.10.1/docs/html/users_guide/pragmas.html'.")

(defvar haskell-completions--keywords
  (list
   "as"
   "case"
   "class"
   "data family"
   "data instance"
   "data"
   "default"
   "deriving instance"
   "deriving"
   "do"
   "else"
   "family"
   "forall"
   "foreign import"
   "foreign"
   "hiding"
   "if"
   "import qualified"
   "import"
   "in"
   "infix"
   "infixl"
   "infixr"
   "instance"
   "let"
   "mdo"
   "module"
   "newtype"
   "of"
   "proc"
   "qualified"
   "rec"
   "signature"
   "then"
   "type family"
   "type instance"
   "type"
   "where")
  "A list of Haskell's keywords (URL `https://wiki.haskell.org/Keywords').
Single char keywords and operator like keywords are not included
in this list.")


(defun haskell-completions-can-grab-prefix ()
  "Check if the case is appropriate for grabbing completion prefix.
Returns t if point is either at whitespace character, or at
punctuation, or at line end and preceeding character is not a
whitespace or new line, otherwise returns nil.

  Returns nil in presence of active region."
  (when (not (region-active-p))
    (when (looking-at-p (rx (| space line-end punct)))
      (when (not (bobp))
        (save-excursion
          (backward-char)
          (not (looking-at-p (rx (| space line-end)))))))))

(defun haskell-completions-grab-pragma-prefix ()
  "Grab completion prefix for pragma completions.
Returns a list of form '(prefix-start-position
prefix-end-position prefix-value prefix-type) for pramga names
such as WARNING, DEPRECATED, LANGUAGE etc.  Also returns
completion prefixes for options in case OPTIONS_GHC pragma, or
language extensions in case of LANGUAGE pragma.  Obsolete OPTIONS
pragma is supported also."
  (when (nth 4 (syntax-ppss))
    ;; We're inside comment
    (let ((p (point))
          (comment-start (nth 8 (syntax-ppss)))
          (case-fold-search nil)
          prefix-start
          prefix-end
          prefix-type
          prefix-value)
      (save-excursion
        (goto-char comment-start)
        (when (looking-at (rx "{-#" (1+ (| space "\n"))))
          (let ((pragma-start (match-end 0)))
            (when (> p pragma-start)
              ;; point stands after `{-#`
              (goto-char pragma-start)
              (when (looking-at (rx (1+ (| upper "_"))))
                ;; found suitable sequence for pragma name
                (let ((pragma-end (match-end 0))
                      (pragma-value (match-string-no-properties 0)))
                  (if (eq p pragma-end)
                      ;; point is at the end of (in)complete pragma name
                      ;; prepare resulting values
                      (progn
                        (setq prefix-start pragma-start)
                        (setq prefix-end pragma-end)
                        (setq prefix-value pragma-value)
                        (setq prefix-type
                              'haskell-completions-pragma-name-prefix))
                    (when (and (> p pragma-end)
                               (or (equal "OPTIONS_GHC" pragma-value)
                                   (equal "OPTIONS" pragma-value)
                                   (equal "LANGUAGE" pragma-value)))
                      ;; point is after pragma name, so we need to check
                      ;; special cases of `OPTIONS_GHC` and `LANGUAGE` pragmas
                      ;; and provide a completion prefix for possible ghc
                      ;; option or language extension.
                      (goto-char pragma-end)
                      (when (re-search-forward
                             (rx (* anything)
                                 (1+ (regexp "\\S-")))
                             p
                             t)
                        (let* ((str (match-string-no-properties 0))
                               (split (split-string str (rx (| space "\n")) t))
                               (val (car (last split)))
                               (end (point)))
                          (when (and (equal p end)
                                     (not (string-match-p "#" val)))
                            (setq prefix-value val)
                            (backward-char (length val))
                            (setq prefix-start (point))
                            (setq prefix-end end)
                            (setq
                             prefix-type
                             (if (not (equal "LANGUAGE" pragma-value))
                                 'haskell-completions-ghc-option-prefix
                               'haskell-completions-language-extension-prefix
                               )))))))))))))
      (when prefix-value
        (list prefix-start prefix-end prefix-value prefix-type)))))

(defun haskell-completions-grab-identifier-prefix ()
  "Grab completion prefix for identifier at point.
Returns a list of form '(prefix-start-position
prefix-end-position prefix-value prefix-type) for haskell
identifier at point depending on result of function
`haskell-ident-pos-at-point'."
  (let ((pos-at-point (haskell-ident-pos-at-point))
        (p (point)))
    (when pos-at-point
      (let* ((start (car pos-at-point))
             (end (cdr pos-at-point))
             (type 'haskell-completions-identifier-prefix)
             (case-fold-search nil)
             value)
        ;; we need end position of result, becase of
        ;; `haskell-ident-pos-at-point' ignores trailing whitespace, e.g. the
        ;; result will be same for `map|` and `map  |` invocations.
        (when (<= p end)
          (setq end p)
          (setq value (buffer-substring-no-properties start end))
          (when (string-match-p (rx bos upper) value)
            ;; we need to check if found identifier is a module name
            (save-excursion
              (goto-char (line-beginning-position))
              (when (re-search-forward
                     (rx "import"
                         (? (1+ space) "qualified")
                         (1+ space)
                         upper
                         (1+ (| alnum ".")))
                     p    ;; bound
                     t)   ;; no-error
                (if (equal p (point))
                    (setq type 'haskell-completions-module-name-prefix)
                  (when (re-search-forward
                         (rx (| " as " "("))
                         start
                         t)
                    ;; but uppercase ident could occur after `as` keyword, or in
                    ;; module imports after opening parenthesis, in this case
                    ;; restore identifier type again, it's neccessary to
                    ;; distinguish the means of completions retrieval
                    (setq type 'haskell-completions-identifier-prefix))))))
          (when (nth 8 (syntax-ppss))
            ;; eighth element of syntax-ppss result is string or comment start,
            ;; so when it's not nil word at point is inside string or comment,
            ;; return special literal prefix type
            (setq type 'haskell-completions-general-prefix))
          ;; finally take in account minlen if given and return the result
          (when value (list start end value type)))))))

(defun haskell-completions-grab-prefix (&optional minlen)
   "Grab prefix at point for possible completion.
Returns a list of form '(prefix-start-position
prefix-end-position prefix-value prefix-type) depending on
situation, e.g. is it needed to complete pragma, module name,
arbitrary identifier, etc.  Returns nil in case it is
impossible to grab prefix.

Possible prefix types are:

* haskell-completions-pragma-name-prefix
* haskell-completions-ghc-option-prefix
* haskell-completions-language-extension-prefix
* haskell-completions-module-name-prefix
* haskell-completions-identifier-prefix
* haskell-completions-general-prefix

the last type is used in cases when completing things inside comments.

If provided optional MINLEN parameter this function will return
result only if prefix length is not less than MINLEN."
   (when (haskell-completions-can-grab-prefix)
     (let ((prefix (cond
                    ((haskell-completions-grab-pragma-prefix))
                    ((haskell-completions-grab-identifier-prefix)))))
       (cond ((and minlen prefix)
              (when (>= (length (nth 2 prefix)) minlen)
                prefix))
             (prefix prefix)))))

(defun haskell-completions--simple-completions (prefix)
  "Provide a list of completion candidates for given PREFIX.
This function is used internally in
`haskell-completions-completion-at-point' and
`haskell-completions-sync-repl-completion-at-point'.

It provides completions for haskell keywords, language pragmas,
GHC's options, and language extensions.

PREFIX should be a list such one returned by
`haskell-completions-grab-identifier-prefix'."
  (cl-destructuring-bind (beg end _pfx typ) prefix
    (when (not (eql typ 'haskell-completions-general-prefix))
      (let ((candidates
             (cl-case typ
               ('haskell-completions-pragma-name-prefix
                haskell-completions--pragma-names)
               ('haskell-completions-ghc-option-prefix
                haskell-ghc-supported-options)
               ('haskell-completions-language-extension-prefix
                haskell-ghc-supported-extensions)
               (otherwise
                (append (when (bound-and-true-p haskell-tags-on-save)
                          tags-completion-table)
                        haskell-completions--keywords)))))
        (list beg end candidates)))))

;;;###autoload
(defun haskell-completions-completion-at-point ()
  "Provide completion list for thing at point.
This function is used in non-interactive `haskell-mode'.  It
provides completions for haskell keywords, language pragmas,
GHC's options, and language extensions, but not identifiers."
  (let ((prefix (haskell-completions-grab-prefix)))
    (when prefix
      (haskell-completions--simple-completions prefix))))

(defun haskell-completions-sync-repl-completion-at-point ()
  "A completion function used in `interactive-haskell-mode'.
Completion candidates are provided quering current haskell
process, that is sending `:complete repl' command.

Completes all possible things: everything that can be completed
with non-interactive function
`haskell-completions-completion-at-point' plus identifier
completions.

Returns nil if no completions available."
  (let ((prefix-data (haskell-completions-grab-prefix)))
    (when prefix-data
      (cl-destructuring-bind (beg end pfx typ) prefix-data
        (when (and (not (eql typ 'haskell-completions-general-prefix))
                   (or haskell-completions-complete-operators
                       (not (save-excursion
                              (goto-char (1- end))
                              (haskell-mode--looking-at-varsym)))))
          ;; do not complete things in comments
          (if (cl-member
               typ
               '(haskell-completions-pragma-name-prefix
                 haskell-completions-ghc-option-prefix
                 haskell-completions-language-extension-prefix))
              ;; provide simple completions
              (haskell-completions--simple-completions prefix-data)
            ;; only two cases left: haskell-completions-module-name-prefix
            ;; and haskell-completions-identifier-prefix
            (let* ((is-import (eql typ 'haskell-completions-module-name-prefix))
                   (candidates
                    (when (and (haskell-session-maybe)
                               (not (haskell-process-cmd
                                     (haskell-interactive-process)))
                               ;; few possible extra checks would be:
                               ;; (haskell-process-get 'is-restarting)
                               ;; (haskell-process-get 'evaluating)
                               )
                      ;; if REPL is available and not busy try to query it for
                      ;; completions list in case of module name or identifier
                      ;; prefixes
                      (haskell-completions-sync-complete-repl pfx is-import))))
              ;; append candidates with keywords
              (list beg end (append
                             candidates
                             haskell-completions--keywords)))))))))

(defun haskell-completions-sync-complete-repl (prefix &optional import)
  "Return completion list for given PREFIX querying REPL synchronously.
When optional IMPORT argument is non-nil complete PREFIX
prepending \"import \" keyword (useful for module names).  This
function is supposed for internal use."
  (haskell-process-get-repl-completions
   (haskell-interactive-process)
   (if import
       (concat "import " prefix)
     prefix)))

(provide 'haskell-completions)
;;; haskell-completions.el ends here
