;;; kivy-mode.el --- Emacs major mode for editing Kivy files
;;
;; Author: Dean Serenevy <dean@serenevy.net>
;; Version: 0.1.0
;;
;; This document borrowed heavily from yaml-mode.el by Yoshiki Kurihara and
;; Marshall Vandegrift.
;;
;; This file is not part of 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; version 3.

;; 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.

;;; Installation:

;; To install, just drop this file into a directory in your `load-path' and
;; (optionally) byte-compile it. To automatically handle files ending in
;; '.kv', add something like:
;;
;;    (require 'kivy-mode)
;;    (add-to-list 'auto-mode-alist '("\\.kv$" . kivy-mode))
;;
;; to your .emacs file.
;;
;; This mode does not enable electric-indent by default. To get this
;; behavior, either enable electric-indent-mode globally or enable it only
;; for kivy buffers using `kivy-mode-hook':
;;
;;    (add-hook 'kivy-mode-hook
;;     '(lambda ()
;;        (electric-indent-local-mode t)))


;; User definable variables

(defgroup kivy nil
  "Support for the kivy user interface definition format"
  :group 'languages
  :prefix "kivy-")

(defcustom kivy-mode-hook nil
  "*Hook run by `kivy-mode'."
  :type 'hook
  :group 'kivy)

(defcustom kivy-indent-offset 4
  "*Amount of offset per level of indentation."
  :type 'integer
  :group 'kivy)

(defcustom kivy-backspace-function 'backward-delete-char-untabify
  "*Function called by `kivy-electric-backspace' when deleting backwards."
  :type 'function
  :group 'kivy)

(defface kivy-tab-face
  '((((class color)) (:background "red" :foreground "red" :bold t))
    (t (:reverse-video t)))
  "Face to use for highlighting tabs in kivy files."
  :group 'faces
  :group 'kivy)

(defcustom kivy-imenu-generic-expression
  '((nil  "^\\([<>a-zA-Z_-]+\\):"          1))
  "The imenu regex to parse an outline of the kivy file."
  :type 'string
  :group 'kivy)


;; Constants

(defconst kivy-mode-version "0.1.0" "Version of `kivy-mode.'")

(defconst kivy-blank-line-re "^ *$"
  "Regexp matching a line containing only (valid) whitespace.")

(defconst kivy-comment-re "\\(?:^\\|\\s-+\\)\\(#.*\\)"
  "Regexp matching a line containing a kivy comment or delimiter.")

(defconst kivy-directive-re "^\\(?:#:\\)\\(\\w+ +.*\\)"
  "Regexp matching a line containing a kivy directive.")

(defconst kivy-tag-re "^ *id: *\\([^ \n]+\\)$"
  "Rexexp matching a kivy tag.")

(defconst kivy-bare-scalar-re
  "\\(?:[^-:,#!\n{\\[ ]\\|[^#!\n{\\[ ]\\S-\\)[^#\n]*?"
  "Rexexp matching a kivy bare scalar.")

(defconst kivy-hash-key-re
  (concat "^ *"
          "\\(" kivy-bare-scalar-re "\\) *:"
          "\\(?: +\\|$\\)")
  "Regexp matching a single kivy hash key.")

(defconst kivy-nested-map-re
  (concat ".*: *$")
  "Regexp matching a line beginning a kivy nested structure.")

(defconst kivy-constant-scalars-re
  (concat "\\(?:^\\|\\(?::\\|-\\|,\\|{\\|\\[\\) +\\) *"
          (regexp-opt
           '("True" "False" "None") t)
          " *$")
  "Regexp matching certain scalar constants in scalar context")



;; Mode setup

(defvar kivy-mode-map ()
  "Keymap used in `kivy-mode' buffers.")
(if kivy-mode-map
    nil
  (setq kivy-mode-map (make-sparse-keymap))
  (define-key kivy-mode-map [backspace] 'kivy-electric-backspace)
  (define-key kivy-mode-map "\C-c<" 'kivy-indent-shift-left)
  (define-key kivy-mode-map "\C-c>" 'kivy-indent-shift-right)
  )

(defvar kivy-mode-syntax-table nil
  "Syntax table in use in kivy-mode buffers.")
(if kivy-mode-syntax-table
    nil
  (setq kivy-mode-syntax-table (make-syntax-table))
  (modify-syntax-entry ?\' "\"" kivy-mode-syntax-table)
  (modify-syntax-entry ?\" "\"" kivy-mode-syntax-table)
  (modify-syntax-entry ?# "<" kivy-mode-syntax-table)
  (modify-syntax-entry ?\n ">" kivy-mode-syntax-table)
  (modify-syntax-entry ?\\ "\\" kivy-mode-syntax-table)
  (modify-syntax-entry ?- "_" kivy-mode-syntax-table)
  (modify-syntax-entry ?_ "w" kivy-mode-syntax-table)
  )


;;;###autoload
(add-to-list 'auto-mode-alist '("\\.kv$" . kivy-mode))


;;;###autoload
(define-derived-mode kivy-mode fundamental-mode "kivy"
  "Simple mode to edit kivy.

\\{kivy-mode-map}"
  (set (make-local-variable 'comment-start) "# ")
  (set (make-local-variable 'comment-start-skip) "#+ *")
  (set (make-local-variable 'indent-line-function) 'kivy-indent-line)
  (set (make-local-variable 'font-lock-defaults)
       '(kivy-font-lock-keywords
         nil nil nil nil
         (font-lock-syntactic-keywords))))


;; Font-lock support

(defvar kivy-font-lock-keywords
  (list
   (cons kivy-comment-re '(1 font-lock-comment-face))
   (cons kivy-constant-scalars-re '(1 font-lock-constant-face))
   (cons kivy-tag-re '(1 font-lock-function-name-face))
   (cons kivy-hash-key-re '(1 font-lock-variable-name-face t))
   (cons kivy-directive-re '(1 font-lock-builtin-face))
   '("^[\t]+" 0 'kivy-tab-face t))
  "Additional expressions to highlight in kivy mode.")

(defvar kivy-font-lock-syntactic-keywords
  (list '())
  "Additional syntax features to highlight in kivy mode.")


;; Indentation and electric keys

(defun kivy-compute-indentation ()
  "Calculate the maximum sensible indentation for the current line."
  (save-excursion
    (beginning-of-line)
    (forward-line -1)
    (while (and (looking-at kivy-blank-line-re)
                (> (point) (point-min)))
      (forward-line -1))
    (+ (current-indentation)
       (if (looking-at kivy-nested-map-re) kivy-indent-offset 0)
       )))

(defun kivy-indent-line ()
  "Indent the current line.
The first time this command is used, the line will be indented to the
maximum sensible indentation.  Each immediately subsequent usage will
back-dent the line by `kivy-indent-offset' spaces.  On reaching column
0, it will cycle back to the maximum sensible indentation."
  (interactive "*")
  (let ((ci (current-indentation))
        (cc (current-column))
        (need (kivy-compute-indentation)))
    (save-excursion
      (beginning-of-line)
      (delete-horizontal-space)
      (if (and (equal last-command this-command) (/= ci 0))
          (indent-to (* (/ (- ci 1) kivy-indent-offset) kivy-indent-offset))
        (indent-to need)))
    (if (< (current-column) (current-indentation))
        (forward-to-indentation 0))))

(defun kivy-electric-backspace (arg)
  "Delete characters or back-dent the current line.
If invoked following only whitespace on a line, will back-dent to the
immediately previous multiple of `kivy-indent-offset' spaces."
  (interactive "*p")
  (if (or (/= (current-indentation) (current-column)) (bolp))
      (funcall kivy-backspace-function arg)
    (let ((ci (current-column)))
      (beginning-of-line)
      (delete-horizontal-space)
      (indent-to (* (/ (- ci (* arg kivy-indent-offset))
                       kivy-indent-offset)
                    kivy-indent-offset)))))


(defun kivy-set-imenu-generic-expression ()
  (make-local-variable 'imenu-generic-expression)
  (make-local-variable 'imenu-create-index-function)
  (setq imenu-create-index-function 'imenu-default-create-index-function)
  (setq imenu-generic-expression kivy-imenu-generic-expression))

(add-hook 'kivy-mode-hook 'kivy-set-imenu-generic-expression)
(add-hook 'kivy-mode-hook
          '(lambda ()
             (setq indent-tabs-mode 'nil)))


(defun kivy-mode-version ()
  "Diplay version of `kivy-mode'."
  (interactive)
  (message "kivy-mode %s" kivy-mode-version)
  kivy-mode-version)

(defun kivy-indent-shift-left (start end &optional count)
  "Shift lines contained in region START END by COUNT columns to the left.
COUNT defaults to `kivy-indent-offset'.  If region isn't
active, the current line is shifted.  The shifted region includes
the lines in which START and END lie.  An error is signaled if
any lines in the region are indented less than COUNT columns."
  (interactive
   (if mark-active
       (list (region-beginning) (region-end) current-prefix-arg)
     (list (line-beginning-position) (line-end-position) current-prefix-arg)))
  (if count
      (setq count (prefix-numeric-value count))
    (setq count kivy-indent-offset))
  (when (> count 0)
    (let ((deactivate-mark nil))
      (save-excursion
        (goto-char start)
        (while (< (point) end)
          (if (and (< (current-indentation) count)
                   (not (looking-at "[ \t]*$")))
              (error "Can't shift all lines enough"))
          (forward-line))
        (indent-rigidly start end (- count))))))

(defun kivy-indent-shift-right (start end &optional count)
  "Shift lines contained in region START END by COUNT columns to the left.
COUNT defaults to `kivy-indent-offset'.  If region isn't
active, the current line is shifted.  The shifted region includes
the lines in which START and END lie."
  (interactive
   (if mark-active
       (list (region-beginning) (region-end) current-prefix-arg)
     (list (line-beginning-position) (line-end-position) current-prefix-arg)))
  (let ((deactivate-mark nil))
    (if count
        (setq count (prefix-numeric-value count))
      (setq count kivy-indent-offset))
    (indent-rigidly start end count)))

(provide 'kivy-mode)

;;; kivy-mode.el ends here
