Files
config-general-mode/config-general-mode.el

456 lines
15 KiB
EmacsLisp
Raw Normal View History

2017-06-21 14:14:43 +02:00
;;; config-general-mode.el --- Config::General config file mode
;; Copyright (C) 2016-2017, T.v.Dein <tlinden@cpan.org>
;; This file is NOT part of Emacs.
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation; either version 2 of the
;; License, or (at your option) any later version.
;; This program is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program; if not, write to the Free Software
;; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
;; USA
;; Version: 0.01
;; Author: T.v.Dein <tlinden@cpan.org>
2017-06-22 20:09:01 +02:00
;; Keywords: config file editing
2017-06-21 14:14:43 +02:00
;; URL: https://github.com/tlinden/config-general-mode
;; License: GNU General Public License >= 2
;;; Commentary:
2017-06-22 20:09:01 +02:00
;;;; Introduction
;; [Config::General](http://search.cpan.org/dist/Config-General/) is a
;; Perl module for parsing config files with some enhanced
;; features. `config-general-mode' makes it easier to edit such config
;; files with emacs.
;; It is based on `conf-mode' with the following features:
;; - good syntax highlighting for config files
;; - completion support with `<tab>' (using `dabbrev')
;; - imenu support for <blocks>
;; - electric paring mode (for quotes, parens, etc) enabled
;; - automatic indenting
;; - jump to include file with `<ret>'
;;;; Installation
;; To use, save config-general-mode.el to a directory in your load-path.
;; Add something like this to your config:
;; (require 'config-general-mode)
;; (add-to-list 'auto-mode-alist '("\\.conf$" . config-general-mode))
;; or load it manually, when needed:
;; M-x config-general-mode
;; You can also enable it with a buffer-local variable by adding this as
;; the first line of a config file:
;; # -*-config-general-*-
2017-06-22 20:19:00 +02:00
;;;; Usage
;; Edit your config file as usual. Use `<tab>' for completion of
;; values and variables. Use `C-c C-t' to toggle flags (like true to
2017-06-22 20:19:00 +02:00
;; false). Use `C-c C-=' on a region to automatically align on the `=`
;; character. Use `C-c C-/' to breakup a region with long lines into
;; shorter ones using backslash notation. Use `<C-return>' to visit
;; an included file or (when not on a link) insert a new line below
;; the current one, indent and move point there. Use `<C-k>' to delete
;; lines, including continuation lines or whole blocks.
2017-06-22 20:19:00 +02:00
2017-06-22 20:09:01 +02:00
;;;; Customize
;; You can customize the mode with:
;; M-x customize-group RET config-general-mode RET
;; You can also use hooks to config-general mode as a way to modify or enhance
;; its behavior. The following hooks are available:
;; config-general-mode-hook
;; For example:
;; (add-hook 'config-general-mode-hook 'electric-indent-mode)
2017-06-21 14:14:43 +02:00
;;; Code:
;;;; Dependencies
2017-06-21 14:14:43 +02:00
(require 'sh-script)
(require 'cc-mode)
;;;; Customizables
;; our group
(defgroup config-general nil
"Config::General config file mode."
:prefix "config-general-"
:group 'conf)
2017-06-21 14:14:43 +02:00
(defgroup config-general-faces nil
"Config::General config file mode faces."
:prefix "config-general-"
:group 'faces)
;; vars
(defcustom config-general-electric-return t
"Enable electric return and follow include files."
:group 'config-general
:type 'boolean)
2017-06-22 17:29:25 +02:00
(defcustom config-general-toggle-values
'(("true" . "false")
("false" . "true")
("on" . "off")
("off" . "on")
("yes" . "no")
("no" . "yes")
("enable" . "disable")
("disable" . "enable"))
"Values which can be toggled, for simplicity's sake, add both values."
:group 'config-general
:type 'list)
;; faces
(defface config-general-file-face
'((t (:inherit link)))
"face for include files"
:group 'config-general-faces)
(defface config-general-constant-face
'((t (:inherit font-lock-constant-face)))
"face for include files"
:group 'config-general-faces)
(defface config-general-special-char-face
'((t (:inherit font-lock-regexp-grouping-backslash)))
"face for special characters like < or |"
:group 'config-general-faces)
(defface config-general-keyword-face
'((t (:inherit font-lock-keyword-face))) ;; maybe type?
"face for special keywords like include"
:group 'config-general-faces)
(defface config-general-blockname-face
'((t (:inherit font-lock-function-name-face)))
"face for block names"
:group 'config-general-faces)
(defface config-general-variable-name-face
'((t (:inherit font-lock-variable-name-face)))
"face for variable name definitions"
:group 'config-general-faces)
(defface config-general-interpolating-variable-face
'((t (:inherit font-lock-constant-face)))
"face for variable name definitions"
:group 'config-general-faces)
(defface config-general-escape-char-face
'((t (:inherit font-lock-warning-face)))
"face for escape chars"
:group 'config-general-faces)
2017-06-21 14:14:43 +02:00
;;;; Global Vars
(defconst config-general-mode-version "0.01" "Config::General mode version")
2017-06-21 14:14:43 +02:00
(defvar config-general-font-lock-keywords nil
"Keywords to highlight in CG mode.")
(defvar config-general-mode-abbrev-table nil)
(defvar config-general-imenu-expression
'(
2017-06-24 11:51:49 +02:00
(nil "^ *<\\([a-zA-Z0-9]+.*\\)>" 1 )
)
"Imenu generic expression for Config:General mode. See `imenu-generic-expression'.")
2017-06-21 14:14:43 +02:00
;;;; Public Functions
(defun config-general-reload()
2017-06-22 17:29:25 +02:00
"Simple mode reloader"
2017-06-21 14:14:43 +02:00
(interactive)
(fundamental-mode)
2017-06-21 14:14:43 +02:00
(config-general-mode))
(defun config-general-align-vars (beg end)
2017-06-22 17:29:25 +02:00
"Automatically align variable assignments inside region marked
with BEG and END based on the = character."
2017-06-21 14:14:43 +02:00
(interactive "r")
(align-regexp beg end "\\(\\s-*\\)=" 1 1 nil))
(defun config-general-do-electric-return ()
2017-06-22 17:29:25 +02:00
"If (point) is on an include filename, call
`find-file-at-point' with it, otherwise add a new line below,
indent it and move (point) there."
(interactive)
(if (eq config-general-electric-return t)
(if (eq (get-text-property (point)'face) 'config-general-file-face)
(find-file-at-point)
(config-general-open-line-below))
(newline)))
(defun config-general-open-line-below ()
2017-06-22 17:29:25 +02:00
"Add a new line below, indent it and move (point) there."
(interactive)
(end-of-line)
(newline-and-indent))
2017-06-22 17:29:25 +02:00
(defun config-general-tab-or-complete ()
"Enter a <TAB> or do a dabbrev completion based on (point) position."
(interactive)
(if (looking-back "[-%$_a-zA-Z0-9]")
(dabbrev-completion)
(indent-for-tab-command)))
(defun config-general-toggle-flag ()
"Toggle a value of the list `config-general-toggle-values'."
(interactive)
(let* ((thing (downcase (thing-at-point 'word t)))
(flag (assoc thing config-general-toggle-values))
(A (car (bounds-of-thing-at-point 'word)))
(B (cdr (bounds-of-thing-at-point 'word))))
(when (and thing flag)
(goto-char B)
(backward-kill-word 1)
(insert (cdr flag)))))
2017-06-21 14:14:43 +02:00
;;;; Internal Functions
(defun config-general--fl-beg-eof (limit)
(re-search-forward "<<\\([A-Z0-9]+\\)\n" limit t))
(defun config-general--fl-end-eof (limit)
(re-search-forward "^\\([A-Z0-9]+\\)\n" limit t))
;; via: https://fuco1.github.io/2017-06-11-Font-locking-with-custom-matchers.html
;; however, I removed the dash.el dependency and used a normal regexp
(defun config-general-match-variables-in-quotes (limit)
"Match variables in double-quotes"
(with-syntax-table config-general-mode-syntax-table
(catch 'done
(while (re-search-forward
"\\(?:^\\|[^\\]\\)\\(\\$\\)\\({.+?}\\|[_[:alnum:]]+\\|[-!#$*0?@_]\\)"
limit t)
(let ((SS (nth 3 (syntax-ppss))))
(when SS
(when (= SS 34)
(throw 'done (point)))))))))
2017-06-24 11:51:49 +02:00
(defun config-general-kill-line-or-block-or-continuation (&optional ARG)
"If the current (and the following) line[s] ends with a bare
backslash - the line continuation marker - the current and all
continuing lines will be killed.
2017-06-24 11:51:49 +02:00
If (point) is on a block begin, then kill the whole block. Named
blocks are not supported though.
Otherwise the original `kill-line' will be called with ARG.
The flag `kill-whole-line' will be followed."
(interactive)
(when kill-whole-line
(beginning-of-line))
2017-06-24 11:51:49 +02:00
(let* ((savepos (point))
(end (line-end-position))
(onblock (save-excursion
(backward-char 1) ;; required since re-search-forward ignores 1st char
(re-search-forward "^\s*<\\([a-zA-Z0-9]+\\)>" end t)))
(block nil))
(if onblock
2017-06-24 11:51:49 +02:00
;; we are on a block begin, search a matching block end
(save-excursion
(setq block (match-string-no-properties 1))
2017-06-24 11:51:49 +02:00
(when (re-search-forward (format "^\s*</%s>" block) nil t)
(setq end (point))))
;; else
(save-excursion
(end-of-line)
(while (looking-back "\\\\")
;; we are at a continuation
(forward-line 1)
(end-of-line)
(setq end (point)))))
;; now, 'end is either the original end of line or somewhere below
2017-06-24 11:51:49 +02:00
(if (= (count-lines savepos end) 1)
;; we didn't leave current line, so just forward the call to the original
(kill-line ARG)
;; else, more than 1 line, a block or continuation line, do it ourselfes
(when (> (point-max) end)
;; goto next line to REALLY delete the line[s]
(setq end (+ end 1)))
(kill-region savepos end))))
;;;; Init Functions
2017-06-21 14:14:43 +02:00
(defun config-general--init-syntax ()
;; we need our own syntax table for mixed C++ and Shell comment support
(set-syntax-table
(let ((st (make-syntax-table)))
(modify-syntax-entry ?\/ ". 14n" st)
(modify-syntax-entry ?\* ". 23n" st)
(modify-syntax-entry ?# "<" st)
(modify-syntax-entry ?\n ">" st)
(modify-syntax-entry ?\\ "\\" st)
(modify-syntax-entry ?$ "'" st)
(modify-syntax-entry ?\' "\"\"") ;; make ' electric too
(modify-syntax-entry ?\` "\"\"") ;; make ` electric too
2017-06-21 14:14:43 +02:00
(modify-syntax-entry ?< ".")
(modify-syntax-entry ?> ".")
st)))
(defun config-general--init-font-lock ()
;; better suited to configs
(setq config-general-font-lock-keywords
'(
;; <>
("\\([<>|]+\\)" 1 'config-general-special-char-face)
;; special handling of single or double quoted variables
(config-general-match-variables-in-quotes
(1 'default t)
(2 font-lock-variable-name-face t))
;; <<include ...>>
("^[ \t]*<<\\(include\\) [ \t]*\\(.+?\\)>>*"
(1 'config-general-constant-face)
(2 'config-general-file-face)) ;; FIXME: turn into real link property!
;; include ...
("^[ \t]*\\(include\\) [ \t]*\\(.*\\)"
(1 'config-general-constant-face)
(2 'config-general-file-face))
;; <block ..>
("^\s*</*\\(.+\\)>" 1 'config-general-blockname-face)
;; variable definitions
;; FIXME: add support for -SplitPolicy and -SplitDelimiter and make
;; the = a customizable variable, if possible
("^[ \t]*\\(.+?\\)[ \t]*="
(1 'config-general-variable-name-face))
;; interpolating variables
2017-06-21 14:14:43 +02:00
("\\$\\({#?\\)?\\([[:alpha:]_][[:alnum:]_]*\\|[-#?@!]\\)"
(2 'config-general-interpolating-variable-face))
2017-06-21 14:14:43 +02:00
;; escape char
("\\(\\\\\\)" (1 'config-general-escape-char-face))
2017-06-21 14:14:43 +02:00
))
(set (make-local-variable 'font-lock-defaults)
'(config-general-font-lock-keywords nil t nil nil))
(font-lock-add-keywords nil
'((config-general--fl-beg-eof . 'config-general-constant-face)
(config-general--fl-end-eof . 'config-general-constant-face))))
2017-06-21 14:14:43 +02:00
(defun config-general--init-minors ()
;; enable simple outlining
(setq outline-heading-alist '(("##" . 1)
("###" . 2)
("####" . 3)
("#####" . 4)))
(outline-minor-mode t)
;; from shell-script-mode, turn << into here-doc
(sh-electric-here-document-mode 1)
;; Inserting a brace or quote automatically inserts the matching pair
(electric-pair-mode t))
(defun config-general--init-vars ()
;; prepare clean startup
(kill-all-local-variables)
;; support for 'comment-region et al
(setq-local comment-start "# ")
(setq-local comment-end "")
;; we don't need a complicated indent strategy, relative is totally ok
(setq-local indent-line-function #'indent-relative)
;; alert about trailing whitespaces, important for continuations
(setq-local show-trailing-whitespace t))
(defun config-general--init-imenu ()
;; imenu config
(make-local-variable 'imenu-generic-expression)
(setq imenu-generic-expression config-general-imenu-expression)
(setq imenu-case-fold-search nil)
(require 'imenu))
2017-06-21 14:14:43 +02:00
;;;###autoload
(defvar config-general-mode-map
(let ((map (make-sparse-keymap)))
2017-06-22 17:29:25 +02:00
(define-key map (kbd "C-c C-7") 'sh-backslash-region) ;; for latin keyboards
(define-key map (kbd "C-c C-/") 'sh-backslash-region)
(define-key map (kbd "C-c C-0") 'config-general-align-vars) ;; for latin keyboards
(define-key map (kbd "C-c C-=") 'config-general-align-vars)
(define-key map (kbd "C-c C-f") 'find-file-at-point) ;; FIXME: change to [follow-link]
(define-key map (kbd "C-c C-t") 'config-general-toggle-flag)
(define-key map (kbd "<C-return>") 'config-general-do-electric-return)
(define-key map (kbd "<tab>") 'config-general-tab-or-complete)
2017-06-24 11:51:49 +02:00
(define-key map (kbd "C-k") 'config-general-kill-line-or-block-or-continuation)
2017-06-21 14:14:43 +02:00
(define-key map [remap delete-backward-char] 'backward-delete-char-untabify)
map)
"Keymap used in Config::General mode."
)
;;;###autoload
(define-derived-mode config-general-mode conf-mode "config-general"
"Config::General config file mode.
\\{config-general-mode-map}"
2017-06-21 14:14:43 +02:00
;; initialize mode
(config-general--init-vars)
2017-06-21 14:14:43 +02:00
(config-general--init-font-lock)
(config-general--init-minors)
(config-general--init-syntax)
(config-general--init-imenu)
2017-06-21 14:14:43 +02:00
;; load keymap
(use-local-map config-general-mode-map)
;; de-activate some (for C::G) senseless bindings
(local-unset-key (kbd "C-c C-c"))
(local-unset-key (kbd "C-c C-j"))
(local-unset-key (kbd "C-c C-p"))
(local-unset-key (kbd "C-c C-u"))
(local-unset-key (kbd "C-c C-w"))
(local-unset-key (kbd "C-c C-x"))
(local-unset-key (kbd "C-c :"))
;; make us known correctly
(setq major-mode 'config-general-mode)
(setq mode-name "C::G")
2017-06-21 14:14:43 +02:00
;; eval hooks, if any
(run-mode-hooks 'config-general-mode-hooks))
;; done
2017-06-21 14:14:43 +02:00
(provide 'config-general-mode)
;;; config-general-mode.el ends here