# HG changeset patch # User Jordi GutiƩrrez Hermoso # Date 1466606363 14400 # Node ID 308651c52a9c7bb5e73f63ee3d80c61fc539b705 # Parent 391b48c4b7110c90a00e29e47bed80ebc5f8a404 add fish-mode diff --git a/dotemacs.el b/dotemacs.el --- a/dotemacs.el +++ b/dotemacs.el @@ -25,6 +25,8 @@ (require 'd-mode) +(require 'fish-mode) + ;; I think I like the idea of the cursor being more indicative of ;; where the point is. (bar-cursor-mode) diff --git a/packages/fish-mode.el b/packages/fish-mode.el new file mode 100644 --- /dev/null +++ b/packages/fish-mode.el @@ -0,0 +1,611 @@ +;;; fish-mode.el --- Major mode for fish shell scripts -*- lexical-binding: t; -*- + +;; Copyright (C) 2014 Tony Wang + +;; Author: Tony Wang +;; Keywords: Fish, shell +;; Package-Requires: ((emacs "24")) + +;; 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 3 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, see . + +;;; Commentary: + +;; A very basic version of major mode for fish shell scripts. +;; Current features: +;; +;; - keyword highlight +;; - basic indent +;; - comment detection +;; - run fish_indent for indention +;; +;; To run fish_indent before save, add the following to init script: +;; (add-hook 'fish-mode-hook (lambda () +;; (add-hook 'before-save-hook 'fish_indent-before-save))) + +;;; Code: + +(unless (fboundp 'setq-local) + (defmacro setq-local (var val) + "Set variable VAR to value VAL in current buffer." + `(set (make-local-variable ',var) ,val))) + +;;; Syntax highlighting +(defconst fish-builtins + (list + "alias" + "bg" + "bind" + "block" + "breakpoint" + "builtin" + "cd" + "commandline" + "command" + "complete" + "contains" + "count" + "dirh" + "dirs" + "echo" + "emit" + "exec" + "fg" + "fish_config" + "fishd" + "fish_indent" + "fish_pager" + "fish_prompt" + "fish_right_prompt" + "fish" + "fish_update_completions" + "funced" + "funcsave" + "functions" + "help" + "history" + "isatty" + "jobs" + "math" + "mimedb" + "nextd" + "open" + "popd" + "prevd" + "psub" + "pushd" + "pwd" + "random" + "read" + "set_color" + "source" + "status" + "trap" + "type" + "ulimit" + "umask" + "vared" + )) +(defconst fish-keywords + (list + "and" + "begin" + "break" + "case" + "continue" + "else" + "end" + "eval" + "exit" + "for" + "function" + "if" + "or" + "return" + "set" + "switch" + "test" + "while" + )) +(defconst fish-font-lock-keywords-1 + (list + + ;; Builtins + `( ,(rx-to-string `(and + symbol-start + (eval `(or ,@fish-builtins)) + symbol-end) + t) + . + font-lock-builtin-face) + + ;; Keywords + `( ,(rx-to-string `(and + symbol-start + (eval `(or ,@fish-keywords)) + symbol-end) + t) + . + font-lock-keyword-face) + + ;; Backslashes + + ;; This doesn't highlight backslashes inside strings. I guess this + ;; is a limitation of using the regexp-based syntax highlighting. + ;; Also, using `(rx (symbol escape))' doesn't match them, even + ;; though I tried adding backslashes to the syntax table as escape + ;; chars. + `( ,(rx + "\\") + . + font-lock-negation-char-face) + + ;; Function definitions + + ;; Using form: + ;; + ;; (MATCHER MATCH-HIGHLIGHT MATCH-ANCHORED) + ;; + ;; The help for "font-lock-keywords" seems to have an error in + ;; which it would mean to use the form "(MATCHER MATCH-ANCHORED)", + ;; which would leave off the MATCH-HIGHLIGHT for the first MATCHER. + ;; However, the example in the help shows the correct form, which + ;; is used here. + + ;; It would be nice to highlight less-important options like + ;; "description" differently than important ones like "on-event", + ;; but I haven't been able to get it working. If I divide the + ;; options into two groups, each group is only matched in order + ;; (i.e. if an option in the second group appears before an option + ;; in the first group, it doesn't match at all). The help for + ;; font-lock-keywords doesn't mention anything about matching + ;; subsequent MATCH-ANCHORED expressions in order, but it appears + ;; to do so. + `( ,(rx symbol-start + "function" + (1+ space) + ;; Function name + (group (1+ (or alnum (syntax symbol)))) + symbol-end) + (1 font-lock-function-name-face) + ;; Function options + (,(rx (group symbol-start + (repeat 1 2 "-") + (1+ (or alnum (syntax symbol))) + symbol-end)) + nil nil + (1 font-lock-negation-char-face))) + + ;; Variable definition + `( ,(rx + symbol-start + "set" + (1+ space) + (optional "-" (repeat 1 2 letter) (1+ space)) + (group (1+ (or alnum (syntax symbol))))) + 1 + font-lock-variable-name-face) + + ;; For loops + `( ,(rx + ;; Beginning of command or line + (or line-start + ";") + (0+ space) + ;; "for" keyword + "for" + (1+ space) + ;; variable name + (group (1+ (or alnum + (syntax symbol)))) + (1+ space) + ;; "in" + (group "in") + (1+ space) + ;; list + (group (or + ;; plain list + (1+ (or alnum + (syntax symbol) + space)) + ;; process substitution + (and + (syntax open-parenthesis) + (+? anything) + (syntax close-parenthesis))))) + (1 font-lock-variable-name-face) + (2 font-lock-keyword-face) + (3 font-lock-string-face t)) + + ;; Variable substitution + `( ,(rx + symbol-start (group "$") (group (1+ (or alnum (syntax symbol)))) symbol-end) + (1 font-lock-string-face) + (2 font-lock-variable-name-face)) + + ;; Negation + `( ,(rx symbol-start + (or (and (group "not") + symbol-end))) + 1 + font-lock-negation-char-face) + + ;; "set" options + `( ,(rx symbol-start (and "set" + (1+ space) + (group (and "-" (repeat 1 2 letter))) + (1+ space))) + 1 + font-lock-negation-char-face) + + ;; Process substitution + `( ,(rx + (1+ space) + (syntax open-parenthesis) + ;; command name + (group (1+ (or alnum (syntax symbol)))) + (0+ not-newline) + (syntax close-parenthesis)) + 1 + ;; It would be nice to use the sh-quoted-exec face, but it's only + ;; available in sh-mode + font-lock-builtin-face) + + ;; Important characters + `( ,(rx symbol-start + (or (any "|&") + (syntax escape)) + ) + . + font-lock-negation-char-face) + + ;; Redirection + `( ,(rx + (1+ space) + (group + (any "><^") + (optional "&") + (optional (1+ space)) + (1+ (not (any space))))) + . + font-lock-negation-char-face) + + ;; Command name + `( ,(rx-to-string + `(and + (or line-start ;; new line + ";" ;; new command + "&" ;; background + "|") ;; pipe + (0+ space) + (optional (eval `(or ,@fish-keywords)) + (1+ space)) + (group (1+ (or alnum (syntax symbol)))) + symbol-end) + t) + 1 + font-lock-builtin-face) + + ;; Numbers + `( ,(rx symbol-start (1+ (or digit (char ?.))) symbol-end) + . + font-lock-constant-face))) + +(defvar fish-mode-syntax-table + (let ((tab (make-syntax-table text-mode-syntax-table))) + (modify-syntax-entry ?\# "<" tab) + (modify-syntax-entry ?\n ">" tab) + (modify-syntax-entry ?\" "\"\"" tab) + (modify-syntax-entry ?\' "\"'" tab) + (modify-syntax-entry ?\\ "\\" tab) + tab) + "Syntax table for `fish-mode'.") + +;;; Indentation helpers + +(defvar fish/block-opening-terms + (mapconcat + 'identity + '("\\" + "\\" + "\\" + "\\" + "\\" + "\\") + "\\|")) + +(defun fish/current-line () + "Return the line at point as a string." + (buffer-substring (line-beginning-position) (line-end-position))) + +(defun fish/fold (f x list) + "Recursively applies (F i j) to LIST starting with X. +For example, (fold F X '(1 2 3)) computes (F (F (F X 1) 2) 3)." + (let ((li list) (x2 x)) + (while li + (setq x2 (funcall f x2 (pop li)))) + x2)) + +(defun fish/count-of-tokens-in-string (token token-to-ignore string) + (let ((count 0) + (pos 0)) + (while pos + (if (and token-to-ignore + (string-match token-to-ignore string pos)) + (setq pos (match-end 0))) + (if (string-match token string pos) + (setq pos (match-end 0) + count (+ count 1)) + (setq pos nil))) + count)) + +(defun fish/at-comment-line? () + "Returns t if looking at comment line, nil otherwise." + (looking-at "[ \t]*#")) + +(defun fish/at-empty-line? () + "Returns t if looking at empty line, nil otherwise." + (looking-at "[ \t]*$")) + +(defun fish/count-of-opening-terms () + (fish/count-of-tokens-in-string fish/block-opening-terms + "\\" + (fish/current-line))) + +(defun fish/count-of-end-terms () + (fish/count-of-tokens-in-string "\\" nil (fish/current-line))) + +(defun fish/at-open-block? () + "Returns t if line contains block opening term + that is not closed in the same line, nil otherwise." + (> (fish/count-of-opening-terms) + (fish/count-of-end-terms))) + +(defun fish/at-open-end? () + "Returns t if line contains 'end' term and + doesn't contain block opening term that matches + this 'end' term. Returns nil otherwise." + (> (fish/count-of-end-terms) + (fish/count-of-opening-terms))) + +(defun fish/line-contains-block-opening-term? () + "Returns t if line contains block opening term, nil otherwise." + (fish/at-open-block?)) + +(defun fish/line-contans-end-term? () + "Returns t if line contains end term, nil otherwise." + (fish/at-open-end?)) + +(defun fish/line-contains-open-switch-term? () + "Returns t if line contains switch term, nil otherwise." + (> (fish/count-of-tokens-in-string "\\" nil (fish/current-line)) + (fish/count-of-end-terms))) + +;;; Indentation + +(defun fish-indent-line () + "Indent current line." + ;; start calculating indentation level + (let ((cur-indent 0) ; indentation level for current line + (rpos (- (point-max) + (point)))) ; used to move point after indentation :: todo - check if it's possible to avoid this variable + (save-excursion + ;; go to beginning of line + (beginning-of-line) + ;; check if already at the beginning of buffer + (unless (bobp) + (cond + ;; found comment line + ;; cur-indent is based on previous non-empty and non-comment line + ;; todo - answer why we can't move it to default case + ((fish/at-comment-line?) + (setq cur-indent (fish-get-normal-indent))) + + ;; found line that starts with 'end' + ;; this is a special case + ;; so get indentation level + ;; from 'fish-get-end-indent function + ((looking-at "[ \t]*end\\>") + (setq cur-indent (fish-get-end-indent))) + + ;; found line that stats with 'case' + ;; this is a special case + ;; so get indentation level + ;; from 'fish-get-case-indent + ((looking-at "[ \t]*case\\>") + (setq cur-indent (fish-get-case-indent))) + + ;; found line that starts with 'else' + ;; cur-indent is previous non-empty and non-comment line + ;; minus tab-width + ((looking-at "[ \t]*else\\>") + (setq cur-indent (- (fish-get-normal-indent) tab-width))) + + ;; default case + ;; cur-indent equals to indentation level of previous + ;; non-empty and non-comment line + (t (setq cur-indent (fish-get-normal-indent)))))) + + ;; before indenting check cur-indent for negative level + (if (< cur-indent 0) (setq cur-indent 0)) + + ;; indent current line + (indent-line-to cur-indent) + + ;; shift point to respect previous position + (if (> (- (point-max) rpos) (point)) + (goto-char (- (point-max) rpos))))) + +(defun fish-get-normal-indent () + "Returns indentation level based on previous non-empty and non-comment line." + (let ((cur-indent 0) + (not-indented t)) + (while (and not-indented + (not (bobp))) + + ;; move to previous line + (forward-line -1) + + (cond + ;; found empty line, so just skip it + ((fish/at-empty-line?)) + + ;; found comment line, so just skip it + ((fish/at-comment-line?)) + + ;; found line that contains an open block + ;; so increase indentation level + ((fish/at-open-block?) + (setq cur-indent (+ (current-indentation) + tab-width) + not-indented nil)) + + ;; found line that starts with 'else' or 'case' + ;; so increase indentation level + ((looking-at "[ \t]*\\(else\\|case\\)\\>") + (setq cur-indent (+ (current-indentation) tab-width) + not-indented nil)) + + ;; found a line that starts with 'end' + ;; so use this line indentation level + ((looking-at "[ \t]*end\\>") + (setq cur-indent (current-indentation) + not-indented nil)) + + ;; found a line that contains open 'end' term + ;; and doesn't start with 'end' (the order matters!) + ;; it means that this 'end' is indented to the right + ;; so we need to decrease indentation level + ((fish/at-open-end?) + (setq cur-indent (- (current-indentation) + tab-width) + not-indented nil)) + + ;; default case + ;; we just set current indentation level + (t + (setq cur-indent (current-indentation) + not-indented nil)))) + cur-indent)) + +(defun fish-get-end-indent () + "Returns indentation level based on matching block opening term." + (let ((cur-indent 0) + (count-of-ends 1)) + (while (not (or (eq count-of-ends 0) + (bobp))) + + ;; move to previous line + (forward-line -1) + + (cond + ;; found empty line, so just skip it + ((fish/at-empty-line?)) + + ;; found comment line, so just skip it + ((fish/at-comment-line?)) + + ;; we found the line that contains unmatched + ;; block opening term so decrease the count of end terms + ((fish/at-open-block?) + (setq count-of-ends (- count-of-ends 1)) + ;; when count of end terms is zero + ;; it means that we found matching term that + ;; opens block + ;; so cur-indent equals to inden equals to + ;; indentation level of current line + (when (eq count-of-ends 0) + (setq cur-indent (current-indentation)))) + + ;; we found new end term + ;; so just increase the count of end terms + ((fish/at-open-end?) + (setq count-of-ends (+ count-of-ends 1))) + + ;; do nothing + (t))) + + ;; it means that we didn't found a matching pair + ;; for 'end' term + (unless (eq count-of-ends 0) + (error "Found unmatched 'end' term.")) + + cur-indent)) + +(defun fish-get-case-indent () + "Returns indentation level based on matching 'switch' term." + (let ((cur-indent 0) + (not-indented t)) + (while (and not-indented + (not (bobp))) + ;; move to previous line + (forward-line -1) + + (cond + ;; found empty line, so just skip it + ((fish/at-empty-line?)) + + ;; found comment line, so just skip it + ((fish/at-comment-line?)) + + ;; line contains switch term + ;; so cur-indent equials to increased + ;; indentation level of current line + ((fish/line-contains-open-switch-term?) + (setq cur-indent (+ (current-indentation) tab-width) + not-indented nil)) + + ;; do nothing + (t))) + + ;; it means that we didn't find a matching pair + (when not-indented + (error "Found 'case' term without matching 'switch' term")) + + cur-indent)) + +;;; fish_indent +(defun fish_indent () + "Indent current buffer using fish_indent" + (interactive) + (let ((current-point (point))) + (call-process-region (point-min) (point-max) "fish_indent" t t nil) + (goto-char current-point) + )) + +;;; Mode definition + +;;;###autoload +(defun fish_indent-before-save () + (interactive) + (when (eq major-mode 'fish-mode) (fish_indent))) + +;;;###autoload +(define-derived-mode fish-mode prog-mode "Fish" + "Major mode for editing fish shell files." + :syntax-table fish-mode-syntax-table + (setq-local indent-line-function 'fish-indent-line) + (setq-local font-lock-defaults '(fish-font-lock-keywords-1)) + (setq-local comment-start "# ") + (setq-local comment-start-skip "#+[\t ]*")) + +;;;###autoload +(add-to-list 'auto-mode-alist '("\\.fish\\'" . fish-mode)) +;;;###autoload +(add-to-list 'auto-mode-alist '("/fish_funced\\..*\\'" . fish-mode)) +;;;###autoload +(add-to-list 'interpreter-mode-alist '("fish" . fish-mode)) + +(provide 'fish-mode) + +;;; fish-mode.el ends here