diff options
Diffstat (limited to 'configs/shared/emacs/.emacs.d/elpa/haskell-mode-20180601.143/haskell-cabal.el')
-rw-r--r-- | configs/shared/emacs/.emacs.d/elpa/haskell-mode-20180601.143/haskell-cabal.el | 1178 |
1 files changed, 1178 insertions, 0 deletions
diff --git a/configs/shared/emacs/.emacs.d/elpa/haskell-mode-20180601.143/haskell-cabal.el b/configs/shared/emacs/.emacs.d/elpa/haskell-mode-20180601.143/haskell-cabal.el new file mode 100644 index 000000000000..35dcca6c597a --- /dev/null +++ b/configs/shared/emacs/.emacs.d/elpa/haskell-mode-20180601.143/haskell-cabal.el @@ -0,0 +1,1178 @@ +;;; haskell-cabal.el --- Support for Cabal packages -*- lexical-binding: t -*- + +;; Copyright © 2007, 2008 Stefan Monnier +;; 2016 Arthur Fayzrakhmanov + +;; Author: Stefan Monnier <monnier@iro.umontreal.ca> + +;; This file is not part of GNU Emacs. + +;; 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: + +;; Todo: + +;; - distinguish continued lines from indented lines. +;; - indent-line-function. +;; - outline-minor-mode. + +;;; Code: + +;; (defun haskell-cabal-extract-fields-from-doc () +;; (require 'xml) +;; (let ((section (completing-read +;; "Section: " +;; '("general-fields" "library" "executable" "buildinfo")))) +;; (goto-char (point-min)) +;; (search-forward (concat "<sect3 id=\"" section "\">"))) +;; (let* ((xml (xml-parse-region +;; (progn (search-forward "<variablelist>") (match-beginning 0)) +;; (progn (search-forward "</variablelist>") (point)))) +;; (varlist (cl-remove-if-not 'consp (cl-cddar xml))) +;; (syms (mapcar (lambda (entry) (cl-caddr (assq 'literal (assq 'term entry)))) +;; varlist)) +;; (fields (mapcar (lambda (sym) (substring-no-properties sym 0 -1)) syms))) +;; fields)) + +(require 'cl-lib) +(require 'haskell-utils) + +(defcustom haskell-hasktags-path "hasktags" + "Path to `hasktags' executable." + :group 'haskell + :type 'string) + +(defcustom haskell-hasktags-arguments '("-e" "-x") + "Additional arguments for `hasktags' executable. +By default these are: + +-e - generate ETAGS file +-x - generate additional information in CTAGS file." + :group 'haskell + :type '(list string)) + +(defconst haskell-cabal-general-fields + ;; Extracted with (haskell-cabal-extract-fields-from-doc "general-fields") + '("name" "version" "cabal-version" "license" "license-file" "copyright" + "author" "maintainer" "stability" "homepage" "package-url" "synopsis" + "description" "category" "tested-with" "build-depends" "data-files" + "extra-source-files" "extra-tmp-files")) + +(defconst haskell-cabal-library-fields + ;; Extracted with (haskell-cabal-extract-fields-from-doc "library") + '("exposed-modules")) + +(defconst haskell-cabal-executable-fields + ;; Extracted with (haskell-cabal-extract-fields-from-doc "executable") + '("executable" "main-is")) + +(defconst haskell-cabal-buildinfo-fields + ;; Extracted with (haskell-cabal-extract-fields-from-doc "buildinfo") + '("buildable" "other-modules" "hs-source-dirs" "extensions" "ghc-options" + "ghc-prof-options" "hugs-options" "nhc-options" "includes" + "install-includes" "include-dirs" "c-sources" "extra-libraries" + "extra-lib-dirs" "cc-options" "ld-options" "frameworks")) + +(defvar haskell-cabal-mode-syntax-table + (let ((st (make-syntax-table))) + ;; The comment syntax can't be described simply in syntax-table. + ;; We could use font-lock-syntactic-keywords, but is it worth it? + ;; (modify-syntax-entry ?- ". 12" st) + (modify-syntax-entry ?\n ">" st) + (modify-syntax-entry ?- "w" st) + st)) + +(defvar haskell-cabal-font-lock-keywords + ;; The comment syntax can't be described simply in syntax-table. + ;; We could use font-lock-syntactic-keywords, but is it worth it? + '(("^[ \t]*--.*" . font-lock-comment-face) + ("^ *\\([^ \t:]+\\):" (1 font-lock-keyword-face)) + ("^\\(Library\\)[ \t]*\\({\\|$\\)" (1 font-lock-keyword-face)) + ("^\\(Executable\\|Test-Suite\\|Benchmark\\)[ \t]+\\([^\n \t]*\\)" + (1 font-lock-keyword-face) (2 font-lock-function-name-face)) + ("^\\(Flag\\)[ \t]+\\([^\n \t]*\\)" + (1 font-lock-keyword-face) (2 font-lock-constant-face)) + ("^\\(Source-Repository\\)[ \t]+\\(head\\|this\\)" + (1 font-lock-keyword-face) (2 font-lock-constant-face)) + ("^ *\\(if\\)[ \t]+.*\\({\\|$\\)" (1 font-lock-keyword-face)) + ("^ *\\(}[ \t]*\\)?\\(else\\)[ \t]*\\({\\|$\\)" + (2 font-lock-keyword-face)) + ("\\<\\(?:True\\|False\\)\\>" + (0 font-lock-constant-face)))) + +(defvar haskell-cabal-buffers nil + "List of Cabal buffers.") + +(defun haskell-cabal-buffers-clean (&optional buffer) + "Refresh list of known cabal buffers. + +Check each buffer in variable `haskell-cabal-buffers' and remove +it from list if one of the following conditions are hold: ++ buffer is killed; ++ buffer's mode is not derived from `haskell-cabal-mode'; ++ buffer is a BUFFER (if given)." + (let ((bufs ())) + (dolist (buf haskell-cabal-buffers) + (if (and (buffer-live-p buf) + (not (eq buf buffer)) + (with-current-buffer buf (derived-mode-p 'haskell-cabal-mode))) + (push buf bufs))) + (setq haskell-cabal-buffers bufs))) + +(defun haskell-cabal-unregister-buffer () + "Exclude current buffer from global list of known cabal buffers." + (haskell-cabal-buffers-clean (current-buffer))) + +;;;###autoload +(add-to-list 'auto-mode-alist '("\\.cabal\\'" . haskell-cabal-mode)) + +(defvar haskell-cabal-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "C-c C-s") 'haskell-cabal-subsection-arrange-lines) + (define-key map (kbd "C-M-n") 'haskell-cabal-next-section) + (define-key map (kbd "C-M-p") 'haskell-cabal-previous-section) + (define-key map (kbd "M-n") 'haskell-cabal-next-subsection) + (define-key map (kbd "M-p") 'haskell-cabal-previous-subsection) + (define-key map (kbd "C-<down>") 'haskell-cabal-next-subsection) + (define-key map (kbd "C-<up>") 'haskell-cabal-previous-subsection) + (define-key map (kbd "C-c C-f") 'haskell-cabal-find-or-create-source-file) + (define-key map (kbd "M-g l") 'haskell-cabal-goto-library-section) + (define-key map (kbd "M-g e") 'haskell-cabal-goto-executable-section) + (define-key map (kbd "M-g b") 'haskell-cabal-goto-benchmark-section) + (define-key map (kbd "M-g t") 'haskell-cabal-goto-test-suite-section) + map)) + +;;;###autoload +(define-derived-mode haskell-cabal-mode fundamental-mode "Haskell-Cabal" + "Major mode for Cabal package description files." + (setq-local font-lock-defaults + '(haskell-cabal-font-lock-keywords t t nil nil)) + (add-to-list 'haskell-cabal-buffers (current-buffer)) + (add-hook 'change-major-mode-hook 'haskell-cabal-unregister-buffer nil 'local) + (add-hook 'kill-buffer-hook 'haskell-cabal-unregister-buffer nil 'local) + (setq-local comment-start "-- ") + (setq-local comment-start-skip "\\(^[ \t]*\\)--[ \t]*") + (setq-local comment-end "") + (setq-local comment-end-skip "[ \t]*\\(\\s>\\|\n\\)") + (setq-local indent-line-function 'haskell-cabal-indent-line) + (setq indent-tabs-mode nil) + ) + +(make-obsolete 'haskell-cabal-get-setting + 'haskell-cabal--get-field + "March 14, 2016") +(defalias 'haskell-cabal-get-setting 'haskell-cabal--get-field + "Try to read value of field with NAME from current buffer. +Obsolete function. Defined for backward compatibility. Use +`haskell-cabal--get-field' instead.") + +(defun haskell-cabal--get-field (name) + "Try to read value of field with NAME from current buffer." + (save-excursion + (let ((case-fold-search t)) + (goto-char (point-min)) + (when (re-search-forward + (concat "^[ \t]*" (regexp-quote name) + ":[ \t]*\\(.*\\(\n[ \t]+[ \t\n].*\\)*\\)") + nil t) + (let ((val (match-string 1)) + (start 1)) + (when (match-end 2) ;Multiple lines. + ;; The documentation is not very precise about what to do about + ;; the \n and the indentation: are they part of the value or + ;; the encoding? I take the point of view that \n is part of + ;; the value (so that values can span multiple lines as well), + ;; and that only the first char in the indentation is part of + ;; the encoding, the rest is part of the value (otherwise, lines + ;; in the value cannot start with spaces or tabs). + (while (string-match "^[ \t]\\(?:\\.$\\)?" val start) + (setq start (1+ (match-beginning 0))) + (setq val (replace-match "" t t val)))) + val))))) + + +(make-obsolete 'haskell-cabal-guess-setting + 'haskell-cabal-get-field + "March 14, 2016") +(defalias 'haskell-cabal-guess-setting 'haskell-cabal-get-field + "Read the value of field with NAME from project's cabal file. +Obsolete function. Defined for backward compatibility. Use +`haskell-cabal-get-field' instead.") + +;;;###autoload +(defun haskell-cabal-get-field (name) + "Read the value of field with NAME from project's cabal file. +If there is no valid .cabal file to get the setting from (or +there is no corresponding setting with that name in the .cabal +file), then this function returns nil." + (interactive) + (when (and name buffer-file-name) + (let ((cabal-file (haskell-cabal-find-file))) + (when (and cabal-file (file-readable-p cabal-file)) + (with-temp-buffer + (insert-file-contents cabal-file) + (haskell-cabal--get-field name)))))) + +;;;###autoload +(defun haskell-cabal-get-dir (&optional use-defaults) + "Get the Cabal dir for a new project. Various ways of figuring this out, + and indeed just prompting the user. Do them all." + (let* ((file (haskell-cabal-find-file)) + (dir (if file (file-name-directory file) default-directory))) + (if use-defaults + dir + (haskell-utils-read-directory-name + (format "Cabal dir%s: " (if file (format " (guessed from %s)" (file-relative-name file)) "")) + dir)))) + +(defun haskell-cabal-compute-checksum (dir) + "Compute MD5 checksum of package description file in DIR. +Return nil if no Cabal description file could be located via +`haskell-cabal-find-pkg-desc'." + (let ((cabal-file (haskell-cabal-find-pkg-desc dir))) + (when cabal-file + (with-temp-buffer + (insert-file-contents cabal-file) + (md5 (buffer-string)))))) + +(defun haskell-cabal-find-file (&optional dir) + "Search for package description file upwards starting from DIR. +If DIR is nil, `default-directory' is used as starting point for +directory traversal. Upward traversal is aborted if file owner +changes. Uses `haskell-cabal-find-pkg-desc' internally." + (let ((use-dir (or dir default-directory))) + (while (and use-dir (not (file-directory-p use-dir))) + (setq use-dir (file-name-directory (directory-file-name use-dir)))) + (when use-dir + (catch 'found + (let ((user (nth 2 (file-attributes use-dir))) + ;; Abbreviate, so as to stop when we cross ~/. + (root (abbreviate-file-name use-dir))) + ;; traverse current dir up to root as long as file owner doesn't change + (while (and root (equal user (nth 2 (file-attributes root)))) + (let ((cabal-file (haskell-cabal-find-pkg-desc root))) + (when cabal-file + (throw 'found cabal-file))) + + (let ((proot (file-name-directory (directory-file-name root)))) + (if (equal proot root) ;; fix-point reached? + (throw 'found nil) + (setq root proot)))) + nil))))) + +(defun haskell-cabal-find-pkg-desc (dir &optional allow-multiple) + "Find a package description file in the directory DIR. +Returns nil if none or multiple \".cabal\" files were found. If +ALLOW-MULTIPLE is non nil, in case of multiple \".cabal\" files, +a list is returned instead of failing with a nil result." + ;; This is basically a port of Cabal's + ;; Distribution.Simple.Utils.findPackageDesc function + ;; http://hackage.haskell.org/packages/archive/Cabal/1.16.0.3/doc/html/Distribution-Simple-Utils.html + ;; but without the exception throwing. + (let* ((cabal-files + (cl-remove-if 'file-directory-p + (cl-remove-if-not 'file-exists-p + (directory-files dir t ".\\.cabal\\'"))))) + (cond + ((= (length cabal-files) 1) (car cabal-files)) ;; exactly one candidate found + (allow-multiple cabal-files) ;; pass-thru multiple candidates + (t nil)))) + +(defun haskell-cabal-find-dir (&optional dir) + "Like `haskell-cabal-find-file' but returns directory instead. +See `haskell-cabal-find-file' for meaning of DIR argument." + (let ((cabal-file (haskell-cabal-find-file dir))) + (when cabal-file + (file-name-directory cabal-file)))) + +;;;###autoload +(defun haskell-cabal-visit-file (other-window) + "Locate and visit package description file for file visited by current buffer. +This uses `haskell-cabal-find-file' to locate the closest +\".cabal\" file and open it. This command assumes a common Cabal +project structure where the \".cabal\" file is in the top-folder +of the project, and all files related to the project are in or +below the top-folder. If called with non-nil prefix argument +OTHER-WINDOW use `find-file-other-window'." + (interactive "P") + ;; Note: We aren't allowed to rely on haskell-session here (which, + ;; in pathological cases, can have a different .cabal file + ;; associated with the current buffer) + (if buffer-file-name + (let ((cabal-file (haskell-cabal-find-file (file-name-directory buffer-file-name)))) + (if cabal-file + (if other-window + (find-file-other-window cabal-file) + (find-file cabal-file)) + (error "Could not locate \".cabal\" file for %S" buffer-file-name))) + (error "Cannot locate \".cabal\" file for buffers not visiting any file"))) + +(defvar haskell-cabal-commands + '("install" + "update" + "list" + "info" + "upgrade" + "fetch" + "unpack" + "check" + "sdist" + "upload" + "report" + "init" + "configure" + "build" + "copy" + "haddock" + "clean" + "hscolour" + "register" + "test" + "help" + "run")) + +;;;###autoload +(defgroup haskell-cabal nil + "Haskell cabal files" + :group 'haskell +) + +(defconst haskell-cabal-section-header-regexp "^[[:alnum:]]" ) +(defconst haskell-cabal-subsection-header-regexp "^[ \t]*[[:alnum:]]\\w*:") +(defconst haskell-cabal-comment-regexp "^[ \t]*--") +(defconst haskell-cabal-empty-regexp "^[ \t]*$") +(defconst haskell-cabal-conditional-regexp "^[ \t]*\\(\\if\\|else\\|}\\)") + +(defun haskell-cabal-classify-line () + "Classify the current line into 'section-header 'subsection-header 'section-data 'comment and 'empty '" + (save-excursion + (beginning-of-line) + (cond + ((looking-at haskell-cabal-subsection-header-regexp ) 'subsection-header) + ((looking-at haskell-cabal-section-header-regexp) 'section-header) + ((looking-at haskell-cabal-comment-regexp) 'comment) + ((looking-at haskell-cabal-empty-regexp ) 'empty) + ((looking-at haskell-cabal-conditional-regexp ) 'conditional) + (t 'section-data)))) + +(defun haskell-cabal-header-p () + "Is the current line a section or subsection header?" + (cl-case (haskell-cabal-classify-line) + ((section-header subsection-header) t))) + +(defun haskell-cabal-section-header-p () + "Is the current line a section or subsection header?" + (cl-case (haskell-cabal-classify-line) + ((section-header) t))) + + +(defun haskell-cabal-section-beginning () + "Find the beginning of the current section" + (save-excursion + (while (not (or (bobp) (haskell-cabal-section-header-p))) + (forward-line -1)) + (point))) + +(defun haskell-cabal-beginning-of-section () + "go to the beginning of the section" + (interactive) + (goto-char (haskell-cabal-section-beginning)) +) + +(defun haskell-cabal-section-end () + "Find the end of the current section" + (interactive) + (save-excursion + (if (re-search-forward "\n\\([ \t]*\n\\)*[[:alnum:]]" nil t) + (match-beginning 0) + (point-max)))) + +(defun haskell-cabal-end-of-section () + "go to the end of the section" + (interactive) + (goto-char (haskell-cabal-section-end))) + +(defun haskell-cabal-next-section () + "Go to the next section" + (interactive) + (when (haskell-cabal-section-header-p) (forward-line)) + (while (not (or (eobp) (haskell-cabal-section-header-p))) + (forward-line))) + +(defun haskell-cabal-previous-section () + "Go to the next section" + (interactive) + (when (haskell-cabal-section-header-p) (forward-line -1)) + (while (not (or (bobp) (haskell-cabal-section-header-p))) + (forward-line -1))) + +(defun haskell-cabal-subsection-end () + "find the end of the current subsection" + (save-excursion + (haskell-cabal-beginning-of-subsection) + (forward-line) + (while (and (not (eobp)) + (member (haskell-cabal-classify-line) '(empty section-data))) + (forward-line)) + (unless (eobp) (forward-line -1)) + (while (and (equal (haskell-cabal-classify-line) 'empty) + (not (bobp))) + (forward-line -1)) + (end-of-line) + (point))) + +(defun haskell-cabal-end-of-subsection () + "go to the end of the current subsection" + (interactive) + (goto-char (haskell-cabal-subsection-end))) + +(defun haskell-cabal-section () + "Get the name and data of the associated section" + (save-excursion + (haskell-cabal-beginning-of-section) + (when (and (haskell-cabal-section-header-p) + (looking-at "^\\(\\w+\\)[ \t]*\\(.*\\)$")) + (list :name (match-string-no-properties 1) + :value (match-string-no-properties 2) + :beginning (match-beginning 0) + :end (haskell-cabal-section-end))))) + + +(defun haskell-cabal-subsection () + "Get the name and bounds of of the current subsection" + (save-excursion + (haskell-cabal-beginning-of-subsection) + (when (looking-at "\\([ \t]*\\(\\w*\\):\\)[ \t]*") + (list :name (match-string-no-properties 2) + :beginning (match-end 0) + :end (save-match-data (haskell-cabal-subsection-end)) + :data-start-column (save-excursion (goto-char (match-end 0)) + (current-column) + ))))) + + +(defun haskell-cabal-section-name (section) + (plist-get section :name)) + +(defun haskell-cabal-section-value (section) + (plist-get section :value)) + +(defun haskell-cabal-section-start (section) + (plist-get section :beginning)) + +(defun haskell-cabal-section-data-start-column (section) + (plist-get section :data-start-column)) + +(defun haskell-cabal-map-component-type (component-type) + "Map from cabal file COMPONENT-TYPE to build command component-type." + (let ((component-type (downcase component-type))) + (cond ((equal component-type "executable") "exe") + ((equal component-type "test-suite") "test") + ((equal component-type "benchmark") "bench")))) + +(defun haskell-cabal-enum-targets (&optional process-type) + "Enumerate .cabal targets. PROCESS-TYPE determines the format of the returned target." + (let ((cabal-file (haskell-cabal-find-file)) + (process-type (if process-type process-type 'ghci))) + (when (and cabal-file (file-readable-p cabal-file)) + (with-temp-buffer + (insert-file-contents cabal-file) + (haskell-cabal-mode) + (goto-char (point-min)) + (let ((matches) + (package-name (haskell-cabal--get-field "name"))) + (haskell-cabal-next-section) + (while (not (eobp)) + (if (haskell-cabal-source-section-p (haskell-cabal-section)) + (let* ((section (haskell-cabal-section)) + (component-type (haskell-cabal-section-name section)) + (val (car (split-string + (haskell-cabal-section-value section))))) + (if (equal (downcase component-type) "library") + (let ((lib-target (if (eq 'stack-ghci process-type) + (concat package-name ":lib") + (concat "lib:" package-name)))) + (push lib-target matches)) + (push (concat (when (eq 'stack-ghci process-type) + (concat package-name ":")) + (haskell-cabal-map-component-type component-type) + ":" + val) + matches)))) + (haskell-cabal-next-section)) + (reverse matches)))))) + +(defmacro haskell-cabal-with-subsection (subsection replace &rest funs) + "Copy subsection data into a temporary buffer, save indentation +and execute FORMS + +If REPLACE is non-nil the subsection data is replaced with the +resulting buffer-content" + (let ((section (make-symbol "section")) + (beg (make-symbol "beg")) + (end (make-symbol "end")) + (start-col (make-symbol "start-col")) + (section-data (make-symbol "section-data"))) + `(let* ((,section ,subsection) + (,beg (plist-get ,section :beginning)) + (,end (plist-get ,section :end)) + (,start-col (plist-get ,section :data-start-column)) + (,section-data (buffer-substring ,beg ,end))) + (save-excursion + (prog1 + (with-temp-buffer + (setq indent-tabs-mode nil) + (indent-to ,start-col) + (insert ,section-data) + (goto-char (point-min)) + (prog1 + (progn (haskell-cabal-save-indentation ,@funs)) + (goto-char (point-min)) + (when (looking-at (format "[ ]\\{0,%d\\}" (1+ ,start-col))) + (replace-match "")) + + (setq ,section-data (buffer-substring (point-min) (point-max))))) + ,@(when replace + `((delete-region ,beg ,end) + (goto-char ,beg) + (insert ,section-data)))))))) + +(defmacro haskell-cabal-each-line (&rest fun) + "Execute FORMS on each line" + `(save-excursion + (while (< (point) (point-max)) + ,@fun + (forward-line)))) + +(defun haskell-cabal-chomp-line () + "Remove leading and trailing whitespaces from current line" + (beginning-of-line) + (when (looking-at "^[ \t]*\\([^ \t]\\|\\(?:[^ \t].*[^ \t]\\)\\)[ \t]*$") + (replace-match (match-string 1) nil t) + t)) + + +(defun haskell-cabal-min-indentation (&optional beg end) + "Compute largest common whitespace prefix of each line in between BEG and END" + (save-excursion + (goto-char (or beg (point-min))) + (let ((min-indent nil)) + (while (< (point) (or end (point-max))) + (let ((indent (current-indentation))) + (if (and (not (haskell-cabal-ignore-line-p)) + (or (not min-indent) + (< indent min-indent))) + (setq min-indent indent))) + (forward-line)) + min-indent))) + +(defun haskell-cabal-ignore-line-p () + "Does line only contain whitespaces and comments?" + (save-excursion + (beginning-of-line) + (looking-at "^[ \t]*\\(?:--.*\\)?$"))) + +(defun haskell-cabal-kill-indentation () + "Remove longest common whitespace prefix from each line" + (goto-char (point-min)) + (let ((indent (haskell-cabal-min-indentation))) + (haskell-cabal-each-line (unless (haskell-cabal-ignore-line-p) + (delete-char indent)) ) + indent)) + +(defun haskell-cabal-add-indentation (indent) + (goto-char (point-min)) + (haskell-cabal-each-line + (unless (haskell-cabal-ignore-line-p) + (indent-to indent)))) + + +(defmacro haskell-cabal-save-indentation (&rest funs) + "Strip indentation from each line, execute FORMS and reinstate indentation + so that the indentation of the FIRST LINE matches" + (let ((old-l1-indent (make-symbol "new-l1-indent")) + (new-l1-indent (make-symbol "old-l1-indent"))) + `(let ( (,old-l1-indent (save-excursion + (goto-char (point-min)) + (current-indentation)))) + (unwind-protect + (progn + (haskell-cabal-kill-indentation) + ,@funs) + (progn + (goto-char (point-min)) + (let ((,new-l1-indent (current-indentation))) + (haskell-cabal-add-indentation (- ,old-l1-indent + ,new-l1-indent)))))))) + +(defun haskell-cabal-comma-separatorp (pos) + "Return non-nil when the char at POS is a comma separator. +Characters that are not a comma, or commas inside a commment or +string, are not comma separators." + (when (eq (char-after pos) ?,) + (let ((ss (syntax-ppss pos))) + (not + (or + ;; inside a string + (nth 3 ss) + ;; inside a comment + (nth 4 ss)))))) + +(defun haskell-cabal-strip-list-and-detect-style () + "Strip commas from a comma-separated list. +Detect and return the comma style. The possible options are: + +before: a comma at the start of each line (except the first), e.g. + Foo + , Bar + +after: a comma at the end of each line (except the last), e.g. + Foo, + Bar + +single: everything on a single line, but comma-separated, e.g. + Foo, Bar + +nil: no commas, e.g. + Foo Bar + +If the styles are mixed, the position of the first comma +determines the style. If there is only one element then `after' +style is assumed." + (let (comma-style) + ;; split list items on single line + (goto-char (point-min)) + (while (re-search-forward + "\\([^ \t,\n]\\)[ \t]*\\(,\\)[ \t]*\\([^ \t,\n]\\)" nil t) + (when (haskell-cabal-comma-separatorp (match-beginning 2)) + (setq comma-style 'single) + (replace-match "\\1\n\\3" nil nil))) + ;; remove commas before + (goto-char (point-min)) + (while (re-search-forward "^\\([ \t]*\\),\\([ \t]*\\)" nil t) + (setq comma-style 'before) + (replace-match "" nil nil)) + ;; remove trailing commas + (goto-char (point-min)) + (while (re-search-forward ",[ \t]*$" nil t) + (unless (eq comma-style 'before) + (setq comma-style 'after)) + (replace-match "" nil nil)) + + ;; if there is just one line then set default as 'after + (unless comma-style + (goto-char (point-min)) + (forward-line) + (when (eobp) + (setq comma-style 'after))) + (goto-char (point-min)) + + (haskell-cabal-each-line (haskell-cabal-chomp-line)) + comma-style)) + +(defun haskell-cabal-listify (comma-style) + "Add commas so that the buffer contains a comma-separated list. +Respect the COMMA-STYLE, see +`haskell-cabal-strip-list-and-detect-style' for the possible +styles." + (cl-case comma-style + ('before + (goto-char (point-min)) + (while (haskell-cabal-ignore-line-p) (forward-line)) + (indent-to 2) + (forward-line) + (haskell-cabal-each-line + (unless (haskell-cabal-ignore-line-p) + (insert ", ")))) + ('after + (goto-char (point-max)) + (while (equal 0 (forward-line -1)) + (unless (haskell-cabal-ignore-line-p) + (end-of-line) + (insert ",") + (beginning-of-line)))) + ('single + (goto-char (point-min)) + (while (not (eobp)) + (end-of-line) + (unless (eobp) + (insert ", ") + (delete-char 1) + (just-one-space)))))) + +(defmacro haskell-cabal-with-cs-list (&rest funs) + "Format the buffer so that each line contains a list element. +Respect the comma style." + (let ((comma-style (make-symbol "comma-style"))) + `(let ((,comma-style + (save-excursion + (haskell-cabal-strip-list-and-detect-style)))) + (unwind-protect (progn ,@funs) + (haskell-cabal-listify ,comma-style))))) + + +(defun haskell-cabal-sort-lines-key-fun () + (when (looking-at "[ \t]*--[ \t,]*") + (goto-char (match-end 0))) + nil) + +(defmacro haskell-cabal-save-position (&rest forms) + "Save position as mark, execute FORMs and go back to mark" + `(prog2 + (haskell-cabal-mark) + (progn ,@forms) + (haskell-cabal-goto-mark) + (haskell-cabal-remove-mark))) + +(defun haskell-cabal-sort-lines-depends-compare (key1 key2) + (let* ((key1str (buffer-substring (car key1) (cdr key1))) + (key2str (buffer-substring (car key2) (cdr key2))) + (base-regex "^[ \t]*base\\($\\|[^[:alnum:]-]\\)")) + (cond + ((string-match base-regex key1str) t) + ((string-match base-regex key2str) nil) + (t (string< key1str key2str))))) + +(defun haskell-cabal-subsection-arrange-lines () + "Sort lines of current subsection" + (interactive) + (haskell-cabal-save-position + (let* ((subsection (haskell-cabal-section-name (haskell-cabal-subsection))) + (compare-lines (if (string= (downcase subsection) "build-depends") + 'haskell-cabal-sort-lines-depends-compare + nil))) + (haskell-cabal-with-subsection + (haskell-cabal-subsection) t + (haskell-cabal-with-cs-list + (sort-subr nil 'forward-line 'end-of-line + 'haskell-cabal-sort-lines-key-fun + 'end-of-line + compare-lines + )))))) + +(defun haskell-cabal-subsection-beginning () + "find the beginning of the current subsection" + (save-excursion + (while (and (not (bobp)) + (not (haskell-cabal-header-p))) + (forward-line -1)) + (back-to-indentation) + (point))) + +(defun haskell-cabal-beginning-of-subsection () + "go to the beginning of the current subsection" + (interactive) + (goto-char (haskell-cabal-subsection-beginning))) + +(defun haskell-cabal-next-subsection () + "go to the next subsection" + (interactive) + (if (haskell-cabal-header-p) (forward-line)) + (while (and (not (eobp)) + (not (haskell-cabal-header-p))) + (forward-line)) + (haskell-cabal-forward-to-line-entry)) + +(defun haskell-cabal-previous-subsection () + "go to the previous subsection" + (interactive) + (if (haskell-cabal-header-p) (forward-line -1)) + (while (and (not (bobp)) + (not (haskell-cabal-header-p))) + (forward-line -1)) + (haskell-cabal-forward-to-line-entry) + ) + + +(defun haskell-cabal-find-subsection-by (section pred) + "Find subsection with name NAME" + (save-excursion + (when section (goto-char (haskell-cabal-section-start section))) + (let* ((end (if section (haskell-cabal-section-end) (point-max))) + (found nil)) + (while (and (< (point) end) + (not found)) + (let ((subsection (haskell-cabal-subsection))) + (when (and subsection (funcall pred subsection)) + (setq found subsection))) + (haskell-cabal-next-subsection)) + found))) + +(defun haskell-cabal-find-subsection (section name) + "Find subsection with name NAME" + (let ((downcase-name (downcase name))) + (haskell-cabal-find-subsection-by + section + `(lambda (subsection) + (string= (downcase (haskell-cabal-section-name subsection)) + ,downcase-name))))) + +(defun haskell-cabal-goto-subsection (name) + (let ((subsection (haskell-cabal-find-subsection (haskell-cabal-section) name))) + (when subsection + (goto-char (haskell-cabal-section-start subsection))))) + +(defun haskell-cabal-goto-exposed-modules () + (interactive) + (haskell-cabal-goto-subsection "exposed-modules")) + +(defun haskell-cabal-subsection-entry-list (section name) + "Get the data of a subsection as a list" + (let ((subsection (haskell-cabal-find-subsection section name))) + (when subsection + (haskell-cabal-with-subsection + subsection nil + (haskell-cabal-with-cs-list + (delete-matching-lines + (format "\\(?:%s\\)\\|\\(?:%s\\)" + haskell-cabal-comment-regexp + haskell-cabal-empty-regexp) + (point-min) (point-max)) + (split-string (buffer-substring-no-properties (point-min) (point-max)) + "\n" t)))))) + +(defun haskell-cabal-remove-mark () + (remove-list-of-text-properties (point-min) (point-max) + '(haskell-cabal-marker))) + + +(defun haskell-cabal-mark () + "Mark the current position with the text property haskell-cabal-marker" + (haskell-cabal-remove-mark) + (put-text-property (line-beginning-position) (line-end-position) + 'haskell-cabal-marker 'marked-line) + (put-text-property (point) (1+ (point)) + 'haskell-cabal-marker 'marked)) + + +(defun haskell-cabal-goto-mark () + "Go to marked line" + (let ((marked-pos (text-property-any (point-min) (point-max) + 'haskell-cabal-marker + 'marked)) + (marked-line (text-property-any (point-min) (point-max) + 'haskell-cabal-marker + 'marked-line) ) + ) + (cond (marked-pos (goto-char marked-pos)) + (marked-line (goto-char marked-line))))) + +(defmacro haskell-cabal-with-subsection-line (replace &rest forms) + "Mark line, copy subsection data into a temporary buffer, save indentation +and execute FORMS at the marked line. + +If REPLACE is non-nil the subsection data is replaced with the +resulting buffer-content. Unmark line at the end." + `(progn + (haskell-cabal-mark) + (unwind-protect + (haskell-cabal-with-subsection (haskell-cabal-subsection) ,replace + (haskell-cabal-goto-mark) + ,@forms) + (haskell-cabal-remove-mark)))) + + +(defun haskell-cabal-get-line-content () + (haskell-cabal-with-subsection-line + nil + (haskell-cabal-with-cs-list + (haskell-cabal-goto-mark) + (buffer-substring-no-properties (line-beginning-position) + (line-end-position))))) + +(defun haskell-cabal-module-to-filename (module) + (concat (replace-regexp-in-string "[.]" "/" module ) ".hs")) + +(defconst haskell-cabal-module-sections '("exposed-modules" "other-modules") + "List of sections that contain module names" +) + +(defconst haskell-cabal-file-sections + '("main-is" "c-sources" "data-files" "extra-source-files" + "extra-doc-files" "extra-tmp-files" ) + "List of subsections that contain filenames" + ) + +(defconst haskell-cabal-source-bearing-sections + '("library" "executable" "test-suite" "benchmark")) + +(defun haskell-cabal-source-section-p (section) + (not (not (member (downcase (haskell-cabal-section-name section)) + haskell-cabal-source-bearing-sections)))) + +(defun haskell-cabal-line-filename () + "Expand filename in current line according to the subsection type + +Module names in exposed-modules and other-modules are expanded by replacing each dot (.) in the module name with a foward slash (/) and appending \".hs\" + +Example: Foo.Bar.Quux ==> Foo/Bar/Quux.hs + +Source names from main-is and c-sources sections are left untouched + +" + (let ((entry (haskell-cabal-get-line-content)) + (subsection (downcase (haskell-cabal-section-name + (haskell-cabal-subsection))))) + (cond ((member subsection haskell-cabal-module-sections) + (haskell-cabal-module-to-filename entry)) + ((member subsection haskell-cabal-file-sections) entry)))) + +(defun haskell-cabal-join-paths (&rest args) + "Crude hack to replace f-join" + (mapconcat 'identity args "/") +) + +(defun haskell-cabal-find-or-create-source-file () + "Open the source file this line refers to." + (interactive) + (let* ((src-dirs (append (haskell-cabal-subsection-entry-list + (haskell-cabal-section) "hs-source-dirs") + '(""))) + (base-dir (file-name-directory (buffer-file-name))) + (filename (haskell-cabal-line-filename))) + (when filename + (let ((candidates + (delq nil (mapcar + (lambda (dir) + (let ((file (haskell-cabal-join-paths base-dir + dir + filename))) + (when (and (file-readable-p file) + (not (file-directory-p file))) + file))) + src-dirs)))) + (if (null candidates) + (unwind-protect + (progn + (haskell-mode-toggle-interactive-prompt-state) + (let* ((src-dir + (haskell-cabal-join-paths base-dir + (or (car src-dirs) ""))) + (newfile (haskell-cabal-join-paths src-dir filename)) + (do-create-p (y-or-n-p (format "Create file %s ?" newfile)))) + (when do-create-p + (find-file-other-window newfile )))) + (haskell-mode-toggle-interactive-prompt-state t)) + (find-file-other-window (car candidates))))))) + + +(defun haskell-cabal-find-section-type (type &optional wrap) + (save-excursion + (haskell-cabal-next-section) + (while + (not + (or + (eobp) + (string= + (downcase type) + (downcase (haskell-cabal-section-name (haskell-cabal-section)))))) + (haskell-cabal-next-section)) + (if (eobp) + (if wrap (progn + (goto-char (point-min)) + (haskell-cabal-find-section-type type nil) ) + nil) + (point)))) + +(defun haskell-cabal-goto-section-type (type) + (let ((section (haskell-cabal-find-section-type type t))) + (if section (goto-char section) + (message "No %s section found" type)))) + +(defun haskell-cabal-goto-library-section () + (interactive) + (haskell-cabal-goto-section-type "library")) + +(defun haskell-cabal-goto-test-suite-section () + (interactive) + (haskell-cabal-goto-section-type "test-suite")) + +(defun haskell-cabal-goto-executable-section () + (interactive) + (haskell-cabal-goto-section-type "executable")) + +(defun haskell-cabal-goto-benchmark-section () + (interactive) + (haskell-cabal-goto-section-type "benchmark")) + + + +(defun haskell-cabal-line-entry-column () + "Column at which the line entry starts" + (save-excursion + (cl-case (haskell-cabal-classify-line) + (section-data (beginning-of-line) + (when (looking-at "[ ]*\\(?:,[ ]*\\)?") + (goto-char (match-end 0)) + (current-column))) + (subsection-header + (haskell-cabal-section-data-start-column (haskell-cabal-subsection)))))) + +(defun haskell-cabal-forward-to-line-entry () + "go forward to the beginning of the line entry (but never move backwards)" + (let ((col (haskell-cabal-line-entry-column))) + (when (and col (< (current-column) col)) + (beginning-of-line) + (forward-char col)))) + +(defun haskell-cabal-indent-line () + "Indent current line according to subsection" + (interactive) + (cl-case (haskell-cabal-classify-line) + (section-data + (save-excursion + (let ((indent (haskell-cabal-section-data-start-column + (haskell-cabal-subsection)))) + (indent-line-to indent) + (beginning-of-line) + (when (looking-at "[ ]*\\([ ]\\{2\\},[ ]*\\)") + (replace-match ", " t t nil 1))))) + (empty + (indent-relative))) + (haskell-cabal-forward-to-line-entry)) + +(defun haskell-cabal-map-sections (fun) + "Execute fun over each section, collecting the result" + (save-excursion + (goto-char (point-min)) + (let ((results nil)) + (while (not (eobp)) + (let* ((section (haskell-cabal-section)) + (result (and section (funcall fun (haskell-cabal-section))))) + (when section (setq results (cons result results)))) + (haskell-cabal-next-section)) + (nreverse results)))) + +(defun haskell-cabal-section-add-build-dependency (dependency &optional sort sec) + "Add a build dependency to the build-depends section" + (let* ((section (or sec (haskell-cabal-section))) + (subsection (and section + (haskell-cabal-find-subsection section "build-depends")))) + (when subsection + (haskell-cabal-with-subsection + subsection t + (haskell-cabal-with-cs-list + (insert dependency) + (insert "\n") + (when sort + (goto-char (point-min)) + (sort-subr nil 'forward-line 'end-of-line + 'haskell-cabal-sort-lines-key-fun))))))) + +(defun haskell-cabal-add-build-dependency (dependency &optional sort silent) + "Add the given DEPENDENCY to every section in cabal file. +If SORT argument is given sort dependencies in section after update. +Pass SILENT argument to update all sections without asking user." + (haskell-cabal-map-sections + (lambda (section) + (when (haskell-cabal-source-section-p section) + (unwind-protect + (progn + (when + (or silent + (y-or-n-p (format "Add dependency %s to %s section %s?" + dependency + (haskell-cabal-section-name section) + (haskell-cabal-section-value section)))) + (haskell-cabal-section-add-build-dependency dependency + sort + section)) + nil) + (haskell-mode-toggle-interactive-prompt-state t)))))) + +(defun haskell-cabal-add-dependency + (package &optional version no-prompt sort silent) + "Add PACKAGE to the cabal file. +If VERSION is non-nil it will be appended as a minimum version. +If NO-PROMPT is nil the minimum package version is read from the +minibuffer. When SORT is non-nil the package entries are sorted +afterwards. If SILENT is non-nil the user is prompted for each +source-section." + (interactive + (list (read-from-minibuffer "Package entry: ") nil t t nil)) + (haskell-mode-toggle-interactive-prompt-state) + (unwind-protect + (save-window-excursion + (find-file-other-window (haskell-cabal-find-file)) + (let ((entry (if no-prompt package + (read-from-minibuffer + "Package entry: " + (concat package + (if version (concat " >= " version) "")))))) + (haskell-cabal-add-build-dependency entry sort silent) + (when (or silent (y-or-n-p "Save cabal file? ")) + (save-buffer)))) + ;; unwind + (haskell-mode-toggle-interactive-prompt-state t))) + + +(defun haskell-cabal--find-tags-dir () + "Return a directory where TAGS file will be generated. +Tries to find cabal file first and if succeeds uses its location. +If cabal file not found uses current file directory. If current +buffer not visiting a file returns nil." + (or (haskell-cabal-find-dir) + (when buffer-file-name + (file-name-directory buffer-file-name)))) + +(defun haskell-cabal--compose-hasktags-command (dir) + "Prepare command to execute `hasktags` command in DIR folder. + +To customise the command executed, see `haskell-hasktags-path' +and `haskell-hasktags-arguments'. + +This function takes into account the user's operating system: in case +of Windows it generates a simple command, relying on Hasktags +itself to find source files: + +hasktags --output=DIR\TAGS -x -e DIR + +In other cases it uses `find` command to find all source files +recursively avoiding visiting unnecessary heavy directories like +.git, .svn, _darcs and build directories created by +cabal-install, stack, etc and passes list of found files to Hasktags." + (if (eq system-type 'windows-nt) + (format "%s --output=%s %s %s" + haskell-hasktags-path + (shell-quote-argument (expand-file-name "TAGS" dir)) + (mapconcat #'identity haskell-hasktags-arguments " ") + (shell-quote-argument dir)) + (format "cd %s && %s | %s" + (shell-quote-argument dir) + (concat "find . " + "-type d \\( " + "-path ./.git " + "-o -path ./.svn " + "-o -path ./_darcs " + "-o -path ./.stack-work " + "-o -path ./dist " + "-o -path ./.cabal-sandbox " + "\\) -prune " + "-o -type f \\( " + "-name '*.hs' " + "-or -name '*.lhs' " + "-or -name '*.hsc' " + "\\) -not \\( " + "-name '#*' " + "-or -name '.*' " + "\\) -print0") + (format "xargs -0 %s %s" + (shell-quote-argument haskell-hasktags-path) + (mapconcat #'identity haskell-hasktags-arguments " "))))) + +(provide 'haskell-cabal) +;;; haskell-cabal.el ends here |