# HG changeset patch # User Gerd Moellmann # Date 939731438 0 # Node ID 29aeb35781cdd9548f3e0871600b7ff025b19974 # Parent ffd53bfb4222805c6d875c889b6c8c5423fb6a53 Added support for indenting existing scripts. (sh-mode-map): Added new bindings. (sh-mode): Updated mode doc-string for new commands, added make-local-variable calls, initialize mode-specific variables. (sh-indent-line): Renamed to sh-basic-indent-line; sh-indent-line is now a different function. (sh-header-marker): Changed docstring. (sh-set-shell): Initialize mode-specific variables. (sh-case, sh-for, sh-if, sh-indexed-loop, sh-repeat, sh-select) (sh-tmp-file, sh-until, sh-until, sh-while, sh-while-getopts): Changed these define-skeleton calls to work with user-specified indentation settings. (sh-basic-indent-line, sh-blink, sh-calculate-indent) (sh-check-paren-in-case, sh-check-rule, sh-do-nothing) (sh-electric-hash, sh-electric-less, sh-electric-rparen) (sh-find-prev-matching, sh-find-prev-switch, sh-get-indent-info) (sh-get-indent-var-for-line, sh-get-kw, sh-get-word) (sh-goto-match-for-done, sh-goto-matching-case, sh-goto-matching-if) (sh-guess-basic-offset, sh-handle-after-case-label) (sh-handle-prev-case, sh-handle-prev-case-alt-end, sh-handle-prev-do) (sh-handle-prev-done, sh-handle-prev-else, sh-handle-prev-esac) (sh-handle-prev-fi, sh-handle-prev-if, sh-handle-prev-open) (sh-handle-prev-rc-case, sh-handle-prev-then, sh-handle-this-close) (sh-handle-this-do, sh-handle-this-done, sh-handle-this-else) (sh-handle-this-esac, sh-handle-this-fi, sh-handle-this-rc-case) (sh-handle-this-then, sh-help-string-for-variable) (sh-in-comment-or-string, sh-indent-line, sh-learn-buffer-indent) (sh-learn-line-indent, sh-load-style, sh-make-vars-local, sh-mark-init) (sh-mark-line, sh-mkword-regexpr, sh-mkword-regexp) (sh-must-be-shell-mode, sh-must-support-indent, sh-name-style) (sh-prev-line, sh-prev-stmt, sh-prev-thing, sh-read-variable) (sh-remove-our-text-properties, sh-rescan-buffer) (sh-reset-indent-vars-to-global-values, sh-safe-backward-sexp) (sh-safe-forward-sexp, sh-save-styles-to-buffer, sh-scan-buffer) (sh-scan-case, sh-search-word, sh-set-char-syntax) (sh-set-here-doc-region, sh-set-indent, sh-set-var-value) (sh-show-indent, sh-this-is-a-continuation, sh-var-value): New functions. (sh-debug, sh-electric-rparen-needed, sh-here-doc-syntax) (sh-indent-supported, sh-kw, sh-kw-alist, sh-kws-for-done) (sh-learned-buffer-hook, sh-make-vars-local, sh-regexp-for-done) (sh-special-keywords, sh-special-syntax, sh-st-punc, sh-styles-alist) (sh-var-list): New variables. diff -r ffd53bfb4222 -r 29aeb35781cd lisp/progmodes/sh-script.el --- a/lisp/progmodes/sh-script.el Tue Oct 12 12:28:42 1999 +0000 +++ b/lisp/progmodes/sh-script.el Tue Oct 12 12:30:38 1999 +0000 @@ -3,7 +3,7 @@ ;; Copyright (C) 1993, 94, 95, 96, 97, 1999 by Free Software Foundation, Inc. ;; Author: Daniel Pfeiffer -;; Version: 2.0e +;; Version: 2.0f ;; Maintainer: FSF ;; Keywords: languages, unix @@ -38,20 +38,179 @@ ;; - Variables in `"' strings aren't fontified because there's no way of ;; syntactically distinguishing those from `'' strings. +;; Indentation +;; =========== +;; Indentation for rc and es modes is very limited, but for Bourne shells +;; and its derivatives it is quite customizable. +;; +;; The following description applies to sh and derived shells (bash, +;; zsh, ...). +;; +;; There are various customization variables which allow tailoring to +;; a wide variety of styles. Most of these variables are named +;; sh-indent-for-XXX and sh-indent-after-XXX. For example. +;; sh-indent-after-if controls the indenting of a line following +;; an if statement, and sh-indent-for-fi controls the indentation +;; of the line containing the fi. +;; +;; You can set each to a numeric value, but it is often more convenient +;; to a symbol such as `+' which uses the value of variable `sh-basic-offset'. +;; By changing this one variable you can increase or decrease how much +;; indentation there is. Valid symbols: +;; +;; + Indent right by sh-basic-offset +;; - Indent left by sh-basic-offset +;; ++ Indent right twice sh-basic-offset +;; -- Indent left twice sh-basic-offset +;; * Indent right half sh-basic-offset +;; / Indent left half sh-basic-offset. +;; +;; There are 4 commands to help set the indentation variables: +;; +;; `sh-show-indent' +;; This shows what variable controls the indentation of the current +;; line and its value. +;; +;; `sh-set-indent' +;; This allows you to set the value of the variable controlling the +;; current line's indentation. You can enter a number or one of a +;; number of special symbols to denote the value of sh-basic-offset, +;; or its negative, or half it, or twice it, etc. If you've used +;; cc-mode this should be familiar. If you forget which symbols are +;; valid simply press C-h at the prompt. +;; +;; `sh-learn-line-indent' +;; Simply make the line look the way you want it, then invoke this +;; command. It will set the variable to the value that makes the line +;; indent like that. If called with a prefix argument then it will set +;; the value to one of the symbols if applicable. +;; +;; `sh-learn-buffer-indent' +;; This is the deluxe function! It "learns" the whole buffer (use +;; narrowing if you want it to process only part). It outputs to a +;; buffer *indent* any conflicts it finds, and all the variables it has +;; learned. This buffer is a sort of Occur mode buffer, allowing you to +;; easily find where something was set. It is popped to automatically +;; if there are any conflicts found or if `sh-popup-occur-buffer' is +;; non-nil. +;; `sh-indent-comment' will be set if all comments follow the same +;; pattern; if they don't it will be set to nil. +;; Whether `sh-basic-offset' is set is determined by variable +;; `sh-learn-basic-offset'. +;; +;; Unfortunately, `sh-learn-buffer-indent' can take a long time to run +;; (e.g. if there are large case statements). Perhaps it does not make +;; sense to run it on large buffers: if lots of lines have different +;; indentation styles it will produce a lot of diagnostics in the +;; *indent* buffer; if there is a consistent style then running +;; `sh-learn-buffer-indent' on a small region of the buffer should +;; suffice. +;; +;; Saving indentation values +;; ------------------------- +;; After you've learned the values in a buffer, how to you remember +;; them? Originally I had hoped that `sh-learn-buffer-indent' +;; would make this unnecessary; simply learn the values when you visit +;; the buffer. +;; You can do this automatically like this: +; (add-hook 'sh-set-shell-hook 'sh-learn-buffer-indent) +;; +;; However... `sh-learn-buffer-indent' is extremely slow, +;; especially on large-ish buffer. Also, if there are conflicts the +;; "last one wins" which may not produce the desired setting. +;; +;; So...There is a minimal way of being able to save indentation values and +;; to reload them in another buffer or at another point in time. +;; +;; Use `sh-name-style' to give a name to the indentation settings of +;; the current buffer. +;; Use `sh-load-style' to load indentation settings for the current +;; buffer from a specific style. +;; Use `sh-save-styles-to-buffer' to write all the styles to a buffer +;; in lisp code. You can then store it in a file and later use +;; `load-file' to load it. +;; +;; Indentation variables - buffer local or global? +;; ---------------------------------------------- +;; I think that often having them buffer-local makes sense, +;; especially if one is using `sh-learn-buffer-indent'. However, if +;; a user sets values using customization, these changes won't appear +;; to work if the variables are already local! +;; +;; To get round this, there is a variable `sh-make-vars-local' and 2 +;; functions: `sh-make-vars-local' and `sh-reset-indent-vars-to-global-values'. +;; +;; If `sh-make-vars-local' is non-nil, then these variables become +;; buffer local when the mode is established. +;; If this is nil, then the variables are global. At any time you +;; can make them local with the command `sh-make-vars-local'. +;; Conversely, to update with the global values you can use the +;; command `sh-reset-indent-vars-to-global-values'. +;; +;; This may be awkward, but the intent is to cover all cases. +;; +;; Awkward things, pitfalls +;; ------------------------ +;; Indentation for a sh script is complicated for a number of reasons: +;; +;; 1. You can't format by simply looking at symbols, you need to look +;; at keywords. [This is not the case for rc and es shells.] +;; 2. The character ")" is used both as a matched pair "(" ... ")" and +;; as a stand-alone symbol (in a case alternative). This makes +;; things quite tricky! +;; 3. Here-documents in a script should be treated "as is", and when +;; they terminate we want to revert to the indentation of the line +;; containing the "<<" symbol. +;; 4. A line may be continued using the "\". +;; 5. The character "#" (outside a string) normally starts a comment, +;; but it doesn't in the sequence "$#"! +;; +;; To try and address points 2 3 and 5 I used a feature that cperl mode +;; uses, that of a text's syntax property. This, however, has 2 +;; disadvantages: +;; 1. We need to scan the buffer to find which ")" symbols belong to a +;; case alternative, to find any here documents, and handle "$#". +;; 2. Setting the text property makes the buffer modified. If the +;; buffer is read-only buffer we have to cheat and bypass the read-only +;; status. This is for cases where the buffer started read-only buffer +;; but the user issued `toggle-read-only'. +;; +;; Bugs +;; ---- +;; - Here-documents are marked with text properties face and syntax +;; table. This serves 2 purposes: stopping indentation while inside +;; them, and moving over them when finding the previous line to +;; indent to. However, if font-lock mode is active when there is +;; any change inside the here-document font-lock clears that +;; property. This causes several problems: lines after the here-doc +;; will not be re-indentation properly, words in the here-doc region +;; may be fontified, and indentation may occur within the +;; here-document. +;; I'm not sure how to fix this, perhaps using the point-entered +;; property. Anyway, if you use font lock and change a +;; here-document, I recommend using M-x sh-rescan-buffer after the +;; changes are made. Similarly, when using higlight-changes-mode, +;; changes inside a here-document may confuse shell indenting, but again +;; using `sh-rescan-buffer' should fix them. +;; +;; - Indenting many lines is slow. It currently does each line +;; independently, rather than saving state information. +;; +;; - `sh-learn-buffer-indent' is extremely slow. +;; +;; Richard Sharman June 1999. + ;;; Code: ;; page 1: variables and settings -;; page 2: mode-command and utility functions -;; page 3: statement syntax-commands for various shells -;; page 4: various other commands +;; page 2: indentation stuff +;; page 3: mode-command and utility functions +;; page 4: statement syntax-commands for various shells +;; page 5: various other commands (require 'executable) -(defvar sh-mode-hook nil - "*Hook run by `sh-mode'.") - -(defvar sh-set-shell-hook nil - "*Hook run by `sh-set-shell'.") + (defgroup sh nil "Shell programming utilities" @@ -182,7 +341,7 @@ :type '(repeat (cons (symbol :tag "Shell") regexp)) :group 'sh-script - :version "20.3") + :version "20.4") (defvar sh-shell-variables nil "Alist of shell variable names that should be included in completion. @@ -277,6 +436,10 @@ (define-key map "\C-c\C-i" 'sh-if) (define-key map "\C-c\C-f" 'sh-for) (define-key map "\C-c\C-c" 'sh-case) + (define-key map "\C-c?" 'sh-show-indent) + (define-key map "\C-c=" 'sh-set-indent) + (define-key map "\C-c<" 'sh-learn-line-indent) + (define-key map "\C-c>" 'sh-learn-buffer-indent) (define-key map "=" 'sh-assignment) (define-key map "\C-c+" 'sh-add) @@ -289,8 +452,10 @@ (define-key map "'" 'skeleton-pair-insert-maybe) (define-key map "`" 'skeleton-pair-insert-maybe) (define-key map "\"" 'skeleton-pair-insert-maybe) - - (define-key map "\t" 'sh-indent-line) + (define-key map ")" 'sh-electric-rparen) + (define-key map "<" 'sh-electric-less) + (define-key map "#" 'sh-electric-hash) + (substitute-key-definition 'complete-tag 'comint-dynamic-complete map (current-global-map)) (substitute-key-definition 'newline-and-indent 'sh-newline-and-indent @@ -378,7 +543,7 @@ (defvar sh-header-marker nil - "When non-`nil' is the end of header for prepending by \\[sh-execute-region]. + "When non-nil is the end of header for prepending by \\[sh-execute-region]. That command is also used for setting this variable.") @@ -659,6 +824,343 @@ ;; efficiently. So we only do it properly for `#' in variable references and ;; do it efficiently by anchoring the regexp to the left. '(("\\${?[^}#\n\t ]*\\(##?\\)" 1 (1 . nil)))) + +(defgroup sh-indentation nil + "Variables controlling indentation in shell scripts. + +Note: customizing these variables will not affect existing buffers if +`sh-make-vars-local' is no-nil. See the documentation for +variable `sh-make-vars-local', command `sh-make-vars-local' +and command `sh-reset-indent-vars-to-global-values'." + :group 'sh-script) + + +(defcustom sh-set-shell-hook nil + "*Hook run by `sh-set-shell'." + :type 'hook + :group 'sh-script) + +(defcustom sh-mode-hook nil + "*Hook run by `sh-mode'." + :type 'hook + :group 'sh-script) + +(defcustom sh-learn-basic-offset nil + "*When `sh-guess-basic-offset' should learn `sh-basic-offset'. + +nil mean: never. +t means: only if there seems to be an obvious value. +Anything else means: whenever we have a \"good guess\" as to the value." + :type '(choice + (const :tag "Never" nil) + (const :tag "Only if sure" t) + (const :tag "If have a good guess" usually) + ) + :group 'sh-indentation) + +(defcustom sh-popup-occur-buffer nil + "*Controls when `sh-learn-buffer-indent' poos the *indent* buffer. +If t it is always shown. If nil, it is shown only when there +are conflicts." + :type '(choice + (const :tag "Only when there are conflicts." nil) + (const :tag "Always" t) + ) + :group 'sh-indentation) + +(defcustom sh-blink t + "*If non-nil, `sh-show-indent' shows the line indentation is relative to. +The position on the line is not necessarily meaningful. +In some cases the line will be the matching keyword, but this is not +always the case." + :type 'boolean + :group 'sh-indentation) + +(defcustom sh-first-lines-indent 0 + "*The indentation of the first non-blank non-comment line. +Usually 0 meaning first column. +Can be set to a number, or to nil which means leave it as is." + :type '(choice + (const :tag "Leave as is" nil) + (integer :tag "Column number" + :menu-tag "Indent to this col (0 means first col)" ) + ) + :group 'sh-indentation) + + +(defcustom sh-basic-offset 4 + "*The default indentation incrementation. +This value is used for the + and - symbols in an indentation variable." + :type 'integer + :group 'sh-indentation) + +(defcustom sh-indent-comment nil + "*How a comment line is to be indented. +nil means leave it as it is; +t means indent it as a normal line, aligning it to previous non-blank + non-comment line; +a number means align to that column, e.g. 0 means fist column." + :type '(choice + (const :tag "Leave as is." nil) + (const :tag "Indent as a normal line." t) + (integer :menu-tag "Indent to this col (0 means first col)." + :tag "Indent to column number.") ) + :group 'sh-indentation) + + +(defvar sh-debug nil + "Enable lots of debug messages - if function `sh-debug' is enabled.") + + +;; Uncomment this defun and comment the defmacro for debugging. +;; (defun sh-debug (&rest args) +;; "For debugging: display message ARGS if variable SH-DEBUG is non-nil." +;; (if sh-debug +;; (apply 'message args))) +(defmacro sh-debug (&rest args)) + +(setq sh-symbol-list + '( + (const :tag "+ " :value + + :menu-tag "+ Indent right by sh-basic-offset") + (const :tag "- " :value - + :menu-tag "- Indent left by sh-basic-offset") + (const :tag "++" :value ++ + :menu-tag "++ Indent right twice sh-basic-offset") + (const :tag "--" :value -- + :menu-tag "-- Indent left twice sh-basic-offset") + (const :tag "* " :value * + :menu-tag "* Indent right half sh-basic-offset") + (const :tag "/ " :value / + :menu-tag "/ Indent left half sh-basic-offset") + )) + +(defcustom sh-indent-for-else 0 + "*How much to indent an else relative to an if. Usually 0." + :type `(choice + (integer :menu-tag "A number (positive=>indent right)" + :tag "A number") + (const :tag "--") ;; separator! + ,@ sh-symbol-list + ) + :group 'sh-indentation) + +(setq sh-number-or-symbol-list + (append (list '( + integer :menu-tag "A number (positive=>indent right)" + :tag "A number") + '(const :tag "--") ;; separator + ) + sh-symbol-list)) + +(defcustom sh-indent-for-fi 0 + "*How much to indent a fi relative to an if. Usually 0." + :type `(choice ,@ sh-number-or-symbol-list ) + :group 'sh-indentation) + +(defcustom sh-indent-for-done '0 + "*How much to indent a done relative to its matching stmt. Usually 0." + :type `(choice ,@ sh-number-or-symbol-list ) + :group 'sh-indentation) + +(defcustom sh-indent-after-else '+ + "*How much to indent a statement after an else statement." + :type `(choice ,@ sh-number-or-symbol-list ) + :group 'sh-indentation) + +(defcustom sh-indent-after-if '+ + "*How much to indent a statement after an if statement. +This includes lines after else and elif statements, too, but +does not affect then else elif or fi statements themselves." + :type `(choice ,@ sh-number-or-symbol-list ) + :group 'sh-indentation) + +(defcustom sh-indent-for-then '+ + "*How much to indent an then relative to an if." + :type `(choice ,@ sh-number-or-symbol-list ) + :group 'sh-indentation) + +(defcustom sh-indent-for-do '* + "*How much to indent a do statement. +This is relative to the statement before the do, i.e. the +while until or for statement." + :type `(choice ,@ sh-number-or-symbol-list) + :group 'sh-indentation) + +(defcustom sh-indent-after-do '* +"*How much to indent a line after a do statement. +This is used when the do is the first word of the line. +This is relative to the statement before the do, e.g. a +while for repeat or select statement." + :type `(choice ,@ sh-number-or-symbol-list) + :group 'sh-indentation) + +(defcustom sh-indent-after-loop-construct '+ + "*How much to indent a statement after a loop construct. + +This variable is used when the keyword \"do\" is on the same line as the +loop statement (e.g. \"until\", \"while\" or \"for\"). +If the do is on a line by itself, then `sh-indent-after-do' is used instead." + :type `(choice ,@ sh-number-or-symbol-list) + :group 'sh-indentation) + + +(defcustom sh-indent-after-done 0 + "*How much to indent a statement after a \"done\" keyword. +Normally this is 0, which aligns the \"done\" to the matching +looping construct line. +Setting it non-zero allows you to have the \"do\" statement on a line +by itself and align the done under to do." + :type `(choice ,@ sh-number-or-symbol-list) + :group 'sh-indentation) + +(defcustom sh-indent-for-case-label '+ + "*How much to indent a case label statement. +This is relative to the line containing the case statement." + :type `(choice ,@ sh-number-or-symbol-list) + :group 'sh-indentation) + +(defcustom sh-indent-for-case-alt '++ + "*How much to indent statements after the case label. +This is relative to the line containing the case statement." + :type `(choice ,@ sh-number-or-symbol-list) + :group 'sh-indentation) + + +(defcustom sh-indent-for-continuation '+ + "*How much to indent for a continuation statement." + :type `(choice ,@ sh-number-or-symbol-list) + :group 'sh-indentation) + +(defcustom sh-indent-after-open '+ + "*How much to indent after a line with an opening parenthesis or brace. +For an open paren after a function `sh-indent-after-function' is used." + :type `(choice ,@ sh-number-or-symbol-list) + :group 'sh-indentation) + +(defcustom sh-indent-after-function '+ + "*How much to indent after a function line." + :type `(choice ,@ sh-number-or-symbol-list) + :group 'sh-indentation) + +;; These 2 are for the rc shell: + +(defcustom sh-indent-after-switch '+ + "*How much to indent a case statement relative to the switch statement. +This is for the rc shell." + :type `(choice ,@ sh-number-or-symbol-list) + :group 'sh-indentation) + +(defcustom sh-indent-after-case '+ + "*How much to indent a statement relative to the case statement. +This is for the rc shell." + :type `(choice ,@ sh-number-or-symbol-list) + :group 'sh-indentation) + +(defface sh-heredoc-face + '((((class color) + (background dark)) + (:foreground "yellow" :bold t)) + (((class color) + (background light)) + (:foreground "tan" )) + (t + (:bold t))) + "Face to show a here-document" + :group 'sh-indentation) + +(defface sh-st-face + '((((class color) + (background dark)) + (:foreground "yellow" :bold t)) + (((class color) + (background light)) + (:foreground "tan" )) + (t + (:bold t))) + "Face to show characters with special syntax properties." + :group 'sh-indentation) + + +;; Internal use - not designed to be changed by the user: + +;; These are used for the syntax table stuff (derived from cperl-mode). +;; Note: parse-sexp-lookup-properties must be set to t for it to work. +(defconst sh-here-doc-syntax '(15)) ;; generic string +(defconst sh-st-punc '(1)) +(defconst sh-special-syntax sh-st-punc) + +(defun sh-mkword-regexpr (word) + "Make a regexp which matches WORD as a word. +This specifically excludes an occurance of WORD followed by +punctuation characters like '-'." + (concat word "\\([^-a-z0-9_]\\|$\\)")) + +(defun sh-mkword-regexp (word) + "Make a regexp which matches WORD as a word. +This specifically excludes an occurance of WORD followed by +or preceded by punctuation characters like '-'." + (concat "\\(^\\|[^-a-z0-9_]\\)" word "\\([^-a-z0-9_]\\|$\\)")) + +(setq sh-re-done (sh-mkword-regexpr "done")) + + +(defconst sh-kws-for-done + '( + (sh . ( "while" "until" "for" ) ) + (bash . ( "while" "until" "for" "select" ) ) + (ksh88 . ( "while" "until" "for" "select" ) ) + (zsh . ( "while" "until" "for" "repeat" "select" ) ) + ) + "Which keywords can match the word `done' in this shell." + ) + + +(defconst sh-indent-supported + '( + (sh . t) + (csh . nil) + (rc . t) + ) + "Shell types that shell indenting can do something with." + ) + +(defconst sh-electric-rparen-needed + '( + (sh . t)) + "Non-nil if the shell type needs an electric handling of case alternatives." + ) + +(defconst sh-var-list + '( + sh-basic-offset sh-first-lines-indent sh-indent-after-case + sh-indent-after-do sh-indent-after-done + sh-indent-after-else + sh-indent-after-if + sh-indent-after-loop-construct + sh-indent-after-open + sh-indent-comment + sh-indent-for-case-alt + sh-indent-for-case-label + sh-indent-for-continuation + sh-indent-for-do + sh-indent-for-done + sh-indent-for-else + sh-indent-for-fi + sh-indent-for-then + ) + "A list of variables used by script mode to control indentation. +This list is used when switching between buffer-local and global +values of variables, and for the commands using indenation styles.") + +(defvar sh-make-vars-local t + "*Controls whether indentation variables are local to the buffer. +If non-nil, indentation variables are made local initially. +If nil, you can later make the variables local by invoking +command `sh-make-vars-local'. +The default is t because I assume that in one Emacs session one is +frequently editing existing scripts with different styles.") + ;; mode-command and utility functions @@ -693,6 +1195,15 @@ \\[sh-until] until loop \\[sh-while] while loop +For sh and rc shells indentation commands are: +\\[sh-show-indent] Show the variable controlling this line's indentation. +\\[sh-set-indent] Set then variable controlling this line's indentation. +\\[sh-learn-line-indent] Change the indentation variable so this line +would indent to the way it currently is. +\\[sh-learn-buffer-indent] Set the indentation variables so the +buffer indents as it currently is indendeted. + + \\[backward-delete-char-untabify] Delete backward one position, even if it was a tab. \\[sh-newline-and-indent] Delete unquoted space and indent new line same as this one. \\[sh-end-of-command] Go to end of successive commands. @@ -734,9 +1245,10 @@ (make-local-variable 'sh-shell-variables) (make-local-variable 'sh-shell-variables-initialized) (make-local-variable 'imenu-generic-expression) + (make-local-variable 'sh-electric-rparen-needed-here) + (make-local-variable 'sh-indent-supported-here) (setq major-mode 'sh-mode mode-name "Shell-script" - indent-line-function 'sh-indent-line ;; not very clever, but enables wrapping skeletons around regions indent-region-function (lambda (b e) (save-excursion @@ -765,7 +1277,9 @@ skeleton-further-elements '((< '(- (min sh-indentation (current-column))))) skeleton-filter 'sh-feature - skeleton-newline-indent-rigidly t) + skeleton-newline-indent-rigidly t + sh-electric-rparen-needed-here nil + sh-indent-supported-here nil) (make-local-variable 'parse-sexp-ignore-comments) (setq parse-sexp-ignore-comments t) ;; Parse or insert magic number for exec, and set all variables depending @@ -783,8 +1297,7 @@ (progn ;; If we don't know the shell for this file, set the syntax ;; table anyway, for the user's normal choice of shell. - (set-syntax-table (or (sh-feature sh-mode-syntax-table) - (standard-syntax-table))) + (set-syntax-table (sh-feature sh-mode-syntax-table)) ;; And avoid indent-new-comment-line (at least) losing. (setq comment-start-skip "#+[\t ]*")))) (run-hooks 'sh-mode-hook)) @@ -872,6 +1385,34 @@ ; (and (boundp 'font-lock-mode) ; font-lock-mode ; (font-lock-mode (font-lock-mode 0))) + (if (setq sh-indent-supported-here (sh-feature sh-indent-supported)) + (progn + (message "Setting up indent for shell type %s" sh-shell) + (make-local-variable 'sh-kw-alist) + (make-local-variable 'sh-regexp-for-done) + (make-local-variable 'parse-sexp-lookup-properties) + (setq sh-electric-rparen-needed-here + (sh-feature sh-electric-rparen-needed)) + (setq parse-sexp-lookup-properties t) + (sh-scan-buffer) + (setq sh-kw-alist (sh-feature sh-kw)) + (let ((regexp (sh-feature sh-kws-for-done))) + (if regexp + (setq sh-regexp-for-done + (sh-mkword-regexpr (regexp-opt regexp t))))) + (message "setting up indent stuff") + ;; sh-mode has already made indent-line-function local + ;; but do it in case this is called before that. + (make-local-variable 'indent-line-function) + (setq indent-line-function 'sh-indent-line) + ;; This is very inefficient, but this at least makes indent-region work: + (make-local-variable 'indent-region-function) + (setq indent-region-function nil) + (if sh-make-vars-local + (sh-make-vars-local)) + (message "Indentation setup for shell type %s" sh-shell)) + (message "No indentation for this shell type.") + (setq indent-line-function 'sh-basic-indent-line)) (run-hooks 'sh-set-shell-hook)) @@ -990,7 +1531,7 @@ skeleton) -(defun sh-indent-line () +(defun sh-basic-indent-line () "Indent a line for Sh mode (shell script mode). Indent as far as preceding non-empty line, then by steps of `sh-indentation'. Lines containing only comments are considered empty." @@ -1061,6 +1602,1947 @@ "Is point preceded by an odd number of backslashes?" (eq -1 (% (save-excursion (skip-chars-backward "\\\\")) 2))) +;; Indentation stuff. +(defun sh-must-be-shell-mode () + "Signal an error if not in Shell-script mode." + (unless (eq major-mode 'sh-mode) + (error "This buffer is not in Shell-script mode"))) + +(defun sh-must-support-indent () + "*Signal an error if the shell type for this buffer is not supported. +Also, the buffer must be in Shell-script mode." + (sh-must-be-shell-mode) + (unless sh-indent-supported-here + (error "This buffer's shell type is not supported for this command"))) + +(defun sh-make-vars-local () + "Make the indentation variables local to this buffer. +Normally they already are local. This command is provided in case +variable `sh-make-vars-local' has been set to nil. + +To revert all these variables to the global values, use +command `sh-reset-indent-vars-to-global-values'." + (interactive) + (sh-must-be-shell-mode) + (mapcar 'make-local-variable sh-var-list) + (message "Indentation variable are now local.")) + +(defun sh-reset-indent-vars-to-global-values () + "Reset local indenatation variables to the global values. +Then, if variable `sh-make-vars-local' is non-nil, make them local." + (interactive) + (sh-must-be-shell-mode) + (mapcar 'kill-local-variable sh-var-list) + (if sh-make-vars-local + (mapcar 'make-local-variable sh-var-list))) + + +(defvar sh-kw-alist nil + "A buffer-local, since it is shell-type dependent, list of keywords.") + +(defvar sh-regexp-for-done nil + "A buffer-local regexp to match opening keyword for done.") + +;; Theoretically these are only needed in shell and derived modes. +;; However, the routines which use them are only called in those modes. +(defconst sh-special-keywords "then\\|do") + +;; ( key-word first-on-this on-prev-line ) +;; This is used to set `sh-kw-alist' which is a list of sublists each +;; having 3 elements: +;; a keyword +;; a rule to check when the keyword apepars on "this" line +;; a rule to check when the keyword apepars on "the previous" line +;; The keyword is usually a string and is the first word on a line. +;; If this keyword appears on the line whose indenation is to be +;; calculated, the rule in element 2 is called. If this returns +;; non-zero, the resulting point (which may be changed by the rule) +;; is used as the default indentation. +;; If it returned false or the keyword was not found in the table, +;; then the keyword from the previous line is looked up and the rule +;; in element 3 is called. In this case, however, +;; `sh-get-indent-info' does not stop but may keepp going and test +;; other keywords against rules in element 3. This is because the +;; precending line could have, for example, an opening "if" and an +;; opening "while" keyword and we need to add the indentation offsets +;; for both. +;; +(defconst sh-kw + '( + (sh + ( "if" + nil + sh-handle-prev-if ) + ( "elif" + sh-handle-this-else + sh-handle-prev-else ) + ( "else" + sh-handle-this-else + sh-handle-prev-else ) + ( "fi" + sh-handle-this-fi + sh-handle-prev-fi ) + ( "then" + sh-handle-this-then + sh-handle-prev-then ) + ( "(" + nil + sh-handle-prev-open ) + ( "{" + nil + sh-handle-prev-open ) + ( "[" + nil + sh-handle-prev-open ) + ( "}" + sh-handle-this-close + nil ) + ( ")" + sh-handle-this-close + nil ) + ( "]" + sh-handle-this-close + nil ) + ( "case" + nil + sh-handle-prev-case ) + ( "esac" + sh-handle-this-esac + sh-handle-prev-esac ) + ( case-label + nil ;; ??? + sh-handle-after-case-label ) + ( ";;" + nil ;; ??? + sh-handle-prev-case-alt-end ;; ?? + ) + ( "done" + sh-handle-this-done + sh-handle-prev-done ) + ( "do" + sh-handle-this-do + sh-handle-prev-do ) + ) ;; end of sh + + ;; Note: we don't need specific stuff for bash and zsh shells; + ;; the regexp `sh-regexp-for-done' handles the extra keywords + ;; these shells use. + (rc + ( "{" + nil + sh-handle-prev-open ) + ( "}" + sh-handle-this-close + nil ) + ( "case" + sh-handle-this-rc-case + sh-handle-prev-rc-case ) + ) ;; end of rc + )) + + +(defun sh-help-string-for-variable (var) + "Construct a string for `sh-read-variable' when changing variable VAR ." + (let ((msg (documentation-property var 'variable-documentation)) + (msg2 "")) + (unless (or + (eq var 'sh-first-lines-indent) + (eq var 'sh-indent-comment)) + (setq msg2 + (format "\n +You can enter a number (positive to increase indentenation, +negative to decrease indentation, zero for no change to indentnation). + +Or, you can enter one of the following symbols which are relative to +the value of variable `sh-basic-offset' +which in this buffer is currently %s. + +\t%s." + sh-basic-offset + (mapconcat '(lambda (x) + (nth (1- (length x)) x) ) + sh-symbol-list "\n\t") + ))) + + (concat + ;; The following shows the global not the local value! + ;; (format "Current value of %s is %s\n\n" var (symbol-value var)) + msg msg2))) + +(defun sh-read-variable (var) + "Read a new value for indentation variable VAR." + (interactive "*variable? ") ;; to test + (let ((minibuffer-help-form `(sh-help-string-for-variable + (quote ,var))) + val) + (setq val (read-from-minibuffer + (format "New value for %s (press %s for help): " + var (single-key-description help-char)) + (format "%s" (symbol-value var)) + nil t)) + val)) + + + +(defun sh-in-comment-or-string (start) + "Return non-nil if START is in a comment or string." + (save-excursion + (let (state) + (beginning-of-line) + (setq state (parse-partial-sexp (point) start nil nil nil t)) + (or (nth 3 state)(nth 4 state))))) + +(defun sh-goto-matching-if () + "Go to the matching if for a fi. +This handles nested if..fi pairs." + (let ((found (sh-find-prev-matching "\\bif\\b" "\\bfi\\b" 1))) + (if found + (goto-char found)))) + + +;; Functions named sh-handle-this-XXX are called when the keyword on the +;; line whose indentation is being handled contain XXX; +;; those named sh-handle-prev-XXX are when XXX appears on the prevoius line. + +(defun sh-handle-prev-if () + (list '(+ sh-indent-after-if))) + +(defun sh-handle-this-else () + (if (sh-goto-matching-if) + ;; (list "aligned to if") + (list "aligned to if" '(+ sh-indent-for-else)) + nil + )) + +(defun sh-handle-prev-else () + (if (sh-goto-matching-if) + (list '(+ sh-indent-after-if)) + )) + +(defun sh-handle-this-fi () + (if (sh-goto-matching-if) + (list "aligned to if" '(+ sh-indent-for-fi)) + nil + )) + +(defun sh-handle-prev-fi () + ;; Why do we have this rule? Because we must go back to the if + ;; to get its indent. We may continue back from there. + ;; We return nil because we don't have anything to add to result, + ;; the side affect of setting align-point is all that matters. + ;; we could return a comment (a string) but I can't think of a good one... + (sh-goto-matching-if) + nil) + +(defun sh-handle-this-then () + (let ((p (sh-goto-matching-if))) + (if p + (list '(+ sh-indent-for-then)) + ))) + +(defun sh-handle-prev-then () + (let ((p (sh-goto-matching-if))) + (if p + (list '(+ sh-indent-after-if)) + ))) + +(defun sh-handle-prev-open () + (save-excursion + (let ((x (sh-prev-stmt))) + (if (and x + (progn + (goto-char x) + (or + (looking-at "function\\b") + (looking-at "\\s-*\\S-+\\s-*()") + ))) + (list '(+ sh-indent-after-function)) + (list '(+ sh-indent-after-open))) + ))) + +(defun sh-handle-this-close () + (forward-char 1) ;; move over ")" + (let ((p (sh-safe-backward-sexp))) + (if p + (list "aligned to opening paren") + nil + ))) + +(defun sh-goto-matching-case () + (let ((found (sh-find-prev-matching "\\bcase\\b" "\\besac\\b" 1))) + (if found + (goto-char found)))) + +(defun sh-handle-prev-case () + ;; This is typically called when point is on same line as a case + ;; we shouldn't -- and can't find prev-case + (if (looking-at ".*\\bcase\\b") + (list '(+ sh-indent-for-case-label)) + (error "We don't see to be on a line with a case") ;; debug + )) + +(defun sh-handle-this-esac () + (let ((p (sh-goto-matching-case))) + (if p + (list "aligned to matching case") + nil + ))) + + +(defun sh-handle-prev-esac () + (let ((p (sh-goto-matching-case))) + (if p + (list "matching case") + nil + ))) + +(defun sh-handle-after-case-label () + (let ((p (sh-goto-matching-case))) + (if p + (list '( + sh-indent-for-case-alt )) + nil + ))) + +(defun sh-handle-prev-case-alt-end () + (let ((p (sh-goto-matching-case))) + (if p + (list '( + sh-indent-for-case-label )) + nil + ))) + +(defun sh-safe-backward-sexp () + "Try and do a `backward-sexp', but do not error. +Return new point if successful, nil if an error occurred." + (condition-case nil + (progn + (backward-sexp 1) + (point) ;; return point if successful + ) + (error + (sh-debug "oops!(0) %d" (point)) + nil ;; return nil if fail + ))) + +(defun sh-safe-forward-sexp () + "Try and do a `forward-sexp', but do not error. +Return new point if successful, nil if an error occurred." + (condition-case nil + (progn + (forward-sexp 1) + (point) ;; return point if successful + ) + (error + (sh-debug "oops!(1) %d" (point)) + nil ;; return nil if fail + ))) + +(defun sh-goto-match-for-done () + (let ((found (sh-find-prev-matching sh-regexp-for-done sh-re-done 1))) + (if found + (goto-char found)))) + +(defun sh-handle-this-done () + (if (sh-goto-match-for-done) + (list "aligned to do stmt" '(+ sh-indent-for-done)) + nil + )) + +(defun sh-handle-prev-done () + (if (sh-goto-match-for-done) + (list "previous done") + nil + )) + +(defun sh-handle-this-do () + (let ( (p (sh-goto-match-for-done)) ) + (if p + (list '(+ sh-indent-for-do)) + nil + ))) + +(defun sh-handle-prev-do () + (let ( (p) ) + (cond + ((save-restriction + (narrow-to-region + (point) + (save-excursion + (beginning-of-line) + (point))) + (sh-goto-match-for-done)) + (sh-debug "match for done found on THIS line") + (list '(+ sh-indent-after-loop-construct))) + ((sh-goto-match-for-done) + (sh-debug "match for done found on PREV line") + (list '(+ sh-indent-after-do))) + (t + (message "match for done NOT found") + nil)))) + +;; for rc: +(defun sh-find-prev-switch () + "Find the line for the switch keyword matching this line's case keyword." + (re-search-backward "\\bswitch\\b" nil t)) + +(defun sh-handle-this-rc-case () + (if (sh-find-prev-switch) + (list '(+ sh-indent-after-switch)) + ;; (list '(+ sh-indent-for-case-label)) + nil)) + +(defun sh-handle-prev-rc-case () + (list '(+ sh-indent-after-case))) + +(defun sh-check-rule (n thing) + (let ((rule (nth n (assoc thing sh-kw-alist))) + (val nil)) + (if rule + (progn + (setq val (funcall rule)) + (sh-debug "rule (%d) for %s at %d is %s\n-> returned %s" + n thing (point) rule val))) + val)) + + +(defun sh-get-indent-info () + "Return indent-info for this line. +This is a list. nil means the line is to be left as is. +Otherwise it contains one or more of the following sublists: +\(t NUMBER\) NUMBER is the base location in the buffer that indendation is + relative to. If present, this is always the first of the + sublists. The indentation of the line in question is + derived from the indentation of this point, possibly + modified by subsequent sublists. +\(+ VAR\) +\(- VAR\) Get the value of variable VAR and add to or subtract from + the indentation calculated so far. +\(= VAR\) Get the value of variable VAR and *replace* the + indentation with itss value. This only occurs for + special variables such as `sh-indent-comment'. +STRING This is ignored for the purposes of calculating + indentation, it is printed in certain cases to help show + what the indentation is based on." + ;; See comments before `sh-kw'. + (save-excursion + (let ((prev-kw nil) + (prev-stmt nil) + (have-result nil) + depth-bol depth-eol + this-kw + (state nil) + state-bol + (depth-prev-bol nil) + start + func val + (result nil) + prev-lines-indent + (prev-list nil) + (this-list nil) + (align-point nil) + prev-line-end x) + (beginning-of-line) + ;; Note: setting result to t means we are done and will return nil. + ;;( This function never returns just t.) + (cond + ((equal (get-text-property (point) 'syntax-table) sh-here-doc-syntax) + (setq result t) + (setq have-result t)) + ((looking-at "\\s-*#") ; was (equal this-kw "#") + (if (bobp) + (setq result t);; return nil if 1st line! + (setq result (list '(= sh-indent-comment))) + ;; we still need to get previous line in case + ;; sh-indent-comnent is t (indent as normal) + (setq align-point (sh-prev-line nil)) + (setq have-result nil) + )) + );; cond + + (unless have-result + ;; Continuation lines are handled specially + (if (sh-this-is-a-continuation) + (progn + ;; We assume the line being continued is already + ;; properly indented... + ;; (setq prev-line-end (sh-prev-line)) + (setq align-point (sh-prev-line nil)) + (setq result (list '(+ sh-indent-for-continuation))) + (setq have-result t)) + (beginning-of-line) + (skip-chars-forward " \t") + (setq this-kw (sh-get-kw))) + + ;; Handle "this" keyword: first word on the line we're + ;; calculating indentation info for. + (if this-kw + (if (setq val (sh-check-rule 1 this-kw)) + (progn + (setq align-point (point)) + (sh-debug + "this - setting align-point to %d" align-point) + (setq result (append result val)) + (setq have-result t) + ;; set prev-line to continue processing remainder + ;; of this line as a previous l ine + (setq prev-line-end (point)) + )))) + + (unless have-result + (setq prev-line-end (sh-prev-line 'end))) + + (if prev-line-end + (save-excursion + ;; We start off at beginning of this line. + ;; Scan previous statements while this is <= + ;; start of previous line. + (setq start (point));; for debug only + (goto-char prev-line-end) + (setq x t) + (while (and x (setq x (sh-prev-thing))) + (sh-debug "at %d x is: %s result is: %s" (point) x result) + (cond + ((and (equal x ")") + (equal (get-text-property (1- (point)) 'syntax-table) + sh-special-syntax)) + (sh-debug "Case label) here") + (setq x 'case-label) + (if (setq val (sh-check-rule 2 x)) + (progn + (setq result (append result val)) + (setq align-point (point)))) + (forward-char -1) + (skip-chars-forward "[a-z0-9]*?") + ) + ((string-match "[])}]" x) + (setq x (sh-safe-backward-sexp)) + (if x + (progn + (setq align-point (point)) + (setq result (append result + (list "aligned to opening paren"))) + ))) + ((string-match "[[({]" x) + (sh-debug "Checking special thing: %s" x) + (if (setq val (sh-check-rule 2 x)) + (setq result (append result val))) + (forward-char -1) + (setq align-point (point))) + ((string-match "[\"'`]" x) + (sh-debug "Skipping back for %s" x) + ;; this was oops-2 + (setq x (sh-safe-backward-sexp))) + ((stringp x) + (sh-debug "Checking string %s at %s" x (point)) + (if (setq val (sh-check-rule 2 x)) + ;; (or (eq t (car val)) + ;; (eq t (car (car val)))) + (setq result (append result val))) + ;; not sure about this test Wed Jan 27 23:48:35 1999 + (setq align-point (point)) + (unless (bolp) + (forward-char -1))) + (t + (error "Don't know what to do with %s" x)) + ) + );; while + (sh-debug "result is %s" result) + ) + (sh-debug "No prev line!") + (sh-debug "result: %s align-point: %s" result align-point) + ) + + (if align-point + ;; was: (setq result (append result (list (list t align-point)))) + (setq result (append (list (list t align-point)) result)) + ) + (sh-debug "result is now: %s" result) + + (or result + (if prev-line-end + (setq result (list (list t prev-line-end))) + (setq result (list (list '= 'sh-first-lines-indent))) + )) + + (if (eq result t) + (setq result nil)) + (sh-debug "result is: %s" result) + result + );; let + )) + + +(defun sh-get-indent-var-for-line (&optional info) + "Return the variable controlling indentation for this line. +If there is not [just] one such variable, return a string +indicating the problem. +If INFO is supplied it is used, else it is calculated." + (let ((var nil) + (result nil) + (reason nil) + sym elt) + (or info + (setq info (sh-get-indent-info))) + (if (null info) + (setq result "this line to be left as is") + (while (and info (null result)) + (setq elt (car info)) + (cond + ((stringp elt) + (setq reason elt) + ) + ((not (listp elt)) + (error "sh-get-indent-var-for-line invalid elt: %s" elt)) + ;; so it is a list + ((eq t (car elt)) + );; nothing + ((symbolp (setq sym (nth 1 elt))) + ;; A bit of a kludge - when we see the sh-indent-comment + ;; ignore other variables. Otherwise it is tricky to + ;; "learn" the comment indentation. + (if (eq var 'sh-indent-comment) + (setq result var) + (if var + (setq result + "this line is controlled by more than 1 variable.") + (setq var sym)))) + (t + (error "sh-get-indent-var-for-line invalid list elt: %s" elt))) + (setq info (cdr info)) + )) + (or result + (setq result var)) + (or result + (setq result reason)) + (if (null result) + ;; e.g. just had (t POS) + (setq result "line has default indentation")) + result)) + + + +;; Finding the previous line isn't trivial. +;; We must *always* go back one more and see if that is a continuation +;; line -- it is the PREVIOUS line which is continued, not the one +;; we are going to! +;; Also, we want to treat a whole "here document" as one big line, +;; because we may want to a align to the beginning of it. +;; +;; What we do: +;; - go back a line, if empty repeat +;; - (we are now at a previous non empty line) +;; - save this +;; - if this is in a here-document, go to the beginning of it +;; and save that +;; - go back one more physcial line and see if it is a continuation line +;; - if yes, save it and repeat +;; - if no, go back to where we last saved. +(defun sh-prev-line (&optional end) + "Back to end of previous non-comment non-empty line. +Go to beginning of logical line unless END is non-nil, in which case +we go to the end of the previous line and do not check for continuations." + (sh-must-be-shell-mode) + (let ((going t) + (last-contin-line nil) + (result nil) + bol eol state) + (save-excursion + (beginning-of-line) + (while (and going + (not (bobp)) + (>= 0 (forward-line -1)) + ) + (setq bol (point)) + (end-of-line) + (setq eol (point)) + (save-restriction + (setq state (parse-partial-sexp bol eol nil nil nil t)) + (if (nth 4 state) + (setq eol (nth 8 state))) + (narrow-to-region bol eol) + (goto-char bol) + (cond + ((looking-at "\\s-*$")) + (t + (if end + (setq result eol) + (setq result bol)) + (setq going nil)) + ))) + (if (and result + (equal (get-text-property (1- result) 'syntax-table) + sh-here-doc-syntax)) + (let ((p1 (previous-single-property-change + (1- result) 'syntax-table))) + (if p1 + (progn + (goto-char p1) + (forward-line -1) + (if end + (end-of-line)) + (setq result (point))) + ))) + (unless end + ;; we must check previous lines to see if they are continuation lines + ;; if so, we must return position of first of them + (while (and (sh-this-is-a-continuation) + (>= 0 (forward-line -1))) + (setq result (point))) + (if result + (progn + (goto-char result) + (beginning-of-line) + (skip-chars-forward " \t") + (setq result (point)) + ))) + ) ;; save-excursion + result + )) + + +(defun sh-prev-stmt () + "Return the address of the previous stmt or nil." + ;; This is used when we are trying to find a matching keyword. + ;; Searching backward for the keyword would certainly be quicker, but + ;; it is hard to remove "false matches" -- such as if the keyword + ;; appears in a string or quote. This way is slower, but (I think) safer. + (interactive) + (save-excursion + (let ((going t) + (start (point)) + (found nil) + (prev nil)) + (skip-chars-backward " \t;|&({[") + (while (and (not found) + (not (bobp)) + going) + ;; Do a backward-sexp if possible, else backup bit by bit... + (if (sh-safe-backward-sexp) + (progn + (if (looking-at sh-special-keywords) + (progn + (setq found prev)) + (setq prev (point)) + )) + ;; backward-sexp failed + (if (zerop (skip-chars-backward " \t()[\]{};`'")) + (forward-char -1)) + (if (bolp) + (let ((back (sh-prev-line nil))) + (if back + (goto-char back) + (setq going nil))))) + (unless found + (skip-chars-backward " \t") + (if (or (and (bolp) (not (sh-this-is-a-continuation))) + (eq (char-before) ?\;) + (looking-at "\\s-*[|&]")) + (setq found (point))))) + (if found + (goto-char found)) + (if found + (progn + (skip-chars-forward " \t|&({[") + (setq found (point)))) + (if (>= (point) start) + (progn + (debug "We didn't move!") + (setq found nil)) + (or found + (sh-debug "Did not find prev stmt."))) + found + ))) + + +(defun sh-get-word () + "Get a shell word skipping whitespace from point." + (interactive) + (skip-chars-forward "\t ") + (let ((start (point))) + (while + (if (looking-at "[\"'`]") + (sh-safe-forward-sexp) + ;; (> (skip-chars-forward "^ \t\n\"'`") 0) + (> (skip-chars-forward "-_a-zA-Z\$0-9") 0) + )) + (buffer-substring start (point)) + )) + +(defun sh-prev-thing () + "Return the previous thing this logical line." + ;; This is called when `sh-get-indent-info' is working backwards on + ;; the previous line(s) finding what keywords may be relevant for + ;; indenting. It moves over sexps if possible, and will stop + ;; on a ; and at the beginning of a line if it is not a continuation + ;; line. + ;; + ;; Added a kludge for ";;" + ;; Possible return values: + ;; nil - nothing + ;; a string - possibly a keyword + ;; + (if (bolp) + nil + (let ((going t) + c n + min-point + (start (point)) + (found nil)) + (save-restriction + (narrow-to-region + (if (sh-this-is-a-continuation) + (setq min-point (sh-prev-line nil)) + (save-excursion + (beginning-of-line) + (setq min-point (point)))) + (point)) + (skip-chars-backward " \t;") + (unless (looking-at "\\s-*;;") + (skip-chars-backward "^)}];\"'`({[") + (setq c (char-before)))) + (sh-debug "stopping at %d c is %s start=%d min-point=%d" + (point) c start min-point) + (if (< (point) min-point) + (error "point %d < min-point %d" (point) min-point)) + (cond + ((looking-at "\\s-*;;") + ;; (message "Found ;; !") + ";;") + ((or (eq c ?\n) + (eq c nil) + (eq c ?\;)) + (save-excursion + ;; skip forward over white space newline and \ at eol + (skip-chars-forward " \t\n\\\\") + (sh-debug "Now at %d start=%d" (point) start) + (if (>= (point) start) + (progn + (sh-debug "point: %d >= start: %d" (point) start) + nil) + (sh-get-word)) + )) + (t + ;; c -- return a string + (char-to-string c) + )) + ))) + + +(defun sh-this-is-a-continuation () + "Return non-nil if current line is a continuation of previous line." + (let ((result nil) + bol eol state) + (save-excursion + (if (and (zerop (forward-line -1)) + (looking-at ".*\\\\$")) + (progn + (setq bol (point)) + (end-of-line) + (setq eol (point)) + (setq state (parse-partial-sexp bol eol nil nil nil t)) + (unless (nth 4 state) + (setq result t)) + ))))) + +(defun sh-get-kw (&optional where and-move) + "Return first word of line from WHERE. +If AND-MOVE is non-nil then move to end of word." + (let ((start (point))) + (if where + (goto-char where)) + (prog1 + (buffer-substring (point) + (progn (skip-chars-forward "^ \t\n;")(point))) + (unless and-move + (goto-char start))) + )) + +(defun sh-find-prev-matching (open close &optional depth) + "Find a matching token for a set of opening and closing keywords. +This takes into account that there may be nested open..close pairings. +OPEN and CLOSE are regexps denoting the tokens to be matched. +Optional parameter DEPTH (usually 1) says how many to look for." + (let ((parse-sexp-ignore-comments t) + prev) + (setq depth (or depth 1)) + (save-excursion + (condition-case nil + (while (and + (/= 0 depth) + (not (bobp)) + (setq prev (sh-prev-stmt))) + (goto-char prev) + (save-excursion + (if (looking-at "\\\\\n") + (progn + (forward-char 2) + (skip-chars-forward " \t"))) + (cond + ((looking-at open) + (setq depth (1- depth)) + (sh-debug "found open at %d - depth = %d" (point) depth)) + ((looking-at close) + (setq depth (1+ depth)) + (sh-debug "found close - depth = %d" depth)) + (t + )))) + (error nil)) + (if (eq depth 0) + prev ;; (point) + nil) + ))) + + +(defun sh-var-value (var &optional ignore-error) + "Return the value of variable VAR, interpreting symbols. +It can also return t or nil. +If an illegal value is found, throw an error unless Optional argument +IGNORE-ERROR is non-nil." + (let ((val (symbol-value var))) + (cond + ((numberp val) + val) + ((eq val t) + val) + ((null val) + val) + ((eq val '+) + sh-basic-offset) + ((eq val '-) + (- sh-basic-offset)) + ((eq val '++) + (* 2 sh-basic-offset)) + ((eq val '--) + (* 2 (- sh-basic-offset))) + ((eq val '*) + (/ sh-basic-offset 2)) + ((eq val '/) + (/ (- sh-basic-offset) 2)) + (t + (if ignore-error + (progn + (message "Don't konw how to handle %s's value of %s" var val) + 0) + (error "Don't know how to handle %s's value of %s" var val)) + )))) + +(defun sh-set-var-value (var value &optional no-symbol) + "Set variable VAR to VALUE. +Unless optional argument NO-SYMBOL is non-nil, then if VALUE is +can be represented by a symbol then do so." + (cond + (no-symbol + (set var value)) + ((= value sh-basic-offset) + (set var '+)) + ((= value (- sh-basic-offset)) + (set var '-)) + ((eq value (* 2 sh-basic-offset)) + (set var '++)) + ((eq value (* 2 (- sh-basic-offset))) + (set var '--)) + ((eq value (/ sh-basic-offset 2)) + (set var '*)) + ((eq value (/ (- sh-basic-offset) 2)) + (set var '/)) + (t + (set var value))) + ) + + +(defun sh-calculate-indent (&optional info) + "Return the indentation for the current line. +If INFO is supplied it is used, else it is calculated from current line." + (let ( + (ofs 0) + (base-value 0) + elt a b var val) + (or info + (setq info (sh-get-indent-info))) + (if (null info) + nil + (while info + (sh-debug "info: %s ofs=%s" info ofs) + (setq elt (car info)) + (cond + ((stringp elt) + ;; do nothing? + ) + ((listp elt) + (setq a (car (car info))) + (setq b (nth 1 (car info))) + (cond + ((eq a t) + (save-excursion + (goto-char b) + (setq val (current-indentation))) + (setq base-value val)) + ((symbolp b) + (setq val (sh-var-value b)) + (cond + ((eq a '=) + (cond + ((null val) + ;; no indentation + ;; set info to nil so we stop immediately + (setq base-value nil ofs nil info nil)) + ((eq val t) + ;; indent as normal line + (setq ofs 0)) + (t + ;; The following assume the (t POS) come first! + (setq ofs val base-value 0) + (setq info nil) ;; ? stop now + )) + ) + ((eq a '+) + (setq ofs (+ ofs val))) + ((eq a '-) + (setq ofs (- ofs val))) + (t + (error "sh-calculate-indent invalid a a=%s b=%s" a b)))) + (t + (error "sh-calculate-indent invalid elt: a=%s b=%s" a b))) + ) + (t + (error "sh-calculate-indent invalid elt %s" elt)) + ) + (sh-debug "a=%s b=%s val=%s base-value=%s ofs=%s" + a b val base-value ofs) + (setq info (cdr info)) + ) + ;; return value: + (sh-debug "at end: base-value: %s ofs: %s" base-value ofs) + + (cond + ((or (null base-value)(null ofs)) + nil) + ((and (numberp base-value)(numberp ofs)) + (sh-debug "base (%d) + ofs (%d) = %d" + base-value ofs (+ base-value ofs)) + (+ base-value ofs)) ;; return value + (t + (error "sh-calculate-indent: Help. base-value=%s ofs=%s" + base-value ofs) + nil)) + ))) + + +(defun sh-indent-line () + "Indent the current line." + (interactive) + (sh-must-be-shell-mode) + (let ((indent (sh-calculate-indent)) shift-amt beg end + (pos (- (point-max) (point)))) + (if indent + (progn + (beginning-of-line) + (setq beg (point)) + (skip-chars-forward " \t") + (setq shift-amt (- indent (current-column))) + (if (zerop shift-amt) + nil + (delete-region beg (point)) + (indent-to indent)) + ;; If initial point was within line's indentation, + ;; position after the indentation. Else stay at same point in text. + (if (> (- (point-max) pos) (point)) + (goto-char (- (point-max) pos))) + )))) + + +(defun sh-blink (blinkpos &optional msg) + "Move cursor momentarily to BLINKPOS and display MSG." + ;; We can get here without it being a number on first line + (if (numberp blinkpos) + (save-excursion + (goto-char blinkpos) + (message msg) + (sit-for blink-matching-delay)) + (message msg) + )) + +(defun sh-show-indent (arg) + "Show the how the currently line would be indented. +This tells you which variable, if any, controls the indentation of +this line. +If optional arg ARG is non-null (called interactively with a prefix), +a pop up window describes this variable. +If variable `sh-blink' is non-nil then momentarily go to the line +we are indenting relative to, if applicable." + (interactive "P") + (sh-must-support-indent) + (let* ((info (sh-get-indent-info)) + (var (sh-get-indent-var-for-line info)) + val msg + (curr-indent (current-indentation)) + ) + (if (stringp var) + (message (setq msg var)) + (setq val (sh-calculate-indent info)) + + (if (eq curr-indent val) + (setq msg (format "%s is %s" var (symbol-value var))) + (setq msg + (if val + (format "%s (%s) would change indent from %d to: %d" + var (symbol-value var) curr-indent val) + (format "%s (%s) would leave line as is" + var (symbol-value var))) + )) + (if (and arg var) + (describe-variable var))) + (if sh-blink + (let ((info (sh-get-indent-info))) + (if (and info (listp (car info)) + (eq (car (car info)) t)) + (sh-blink (nth 1 (car info)) msg) + (message msg))) + (message msg)) + )) + +(defun sh-set-indent () + "Set the indentation for the current line. +If the current line is controlled by an indentation variable, prompt +for a new value for it." + (interactive) + (sh-must-support-indent) + (let* ((info (sh-get-indent-info)) + (var (sh-get-indent-var-for-line info)) + val val0 new-val old-val indent-val) + (if (stringp var) + (message (format "Cannot set indent - %s" var)) + (setq old-val (symbol-value var)) + (setq val (sh-read-variable var)) + (condition-case nil + (progn + (set var val) + (setq indent-val (sh-calculate-indent info)) + (if indent-val + (message "Variable: %s Value: %s would indent to: %d" + var (symbol-value var) indent-val) + (message "Variable: %s Value: %s would leave line as is." + var (symbol-value var))) + ;; I'm not sure about this, indenting it now? + ;; No. Because it would give the impression that an undo would + ;; restore thing, but the value has been altered. + ;; (sh-indent-line) + ) + (error + (set var old-val) + (message "Bad value for %s, restoring to previous value %s" + var old-val) + (sit-for 1) + nil)) + ))) + + +(defun sh-learn-line-indent (arg) + "Learn how to indent a line as it currently is indented. + +If there is an indentation variable which controls this line's indentation, +then set it to a value which would indent the line the way it +presently is. + +If the value can be represented by one of the symbols then do so +unless optional argument ARG (the prefix when interactive) is non-nil." + (interactive "*P") + (sh-must-support-indent) + ;; I'm not sure if we show allow learning on an empty line. + ;; Though it might occasionally be useful I think it usually + ;; would just be confusing. + (if (save-excursion + (beginning-of-line) + (looking-at "\\s-*$")) + (message "sh-learn-line-indent ignores empty lines.") + (let* ((info (sh-get-indent-info)) + (var (sh-get-indent-var-for-line info)) + ival sval diff new-val + (no-symbol arg) + (curr-indent (current-indentation))) + (cond + ((stringp var) + (message (format "Cannot learn line - %s" var))) + ((eq var 'sh-indent-comment) + ;; This is arbitrary... + ;; - if curr-indent is 0, set to curr-indent + ;; - else if it has the indentation of a "normal" line, + ;; then set to t + ;; - else set to curr-indent. + (setq sh-indent-comment + (if (= curr-indent 0) + 0 + (let* ((sh-indent-comment t) + (val2 (sh-calculate-indent info))) + (if (= val2 curr-indent) + t + curr-indent)))) + (message "%s set to %s" var (symbol-value var)) + ) + ((numberp (setq sval (sh-var-value var))) + (setq ival (sh-calculate-indent info)) + (setq diff (- curr-indent ival)) + + (sh-debug "curr-indent: %d ival: %d diff: %d var:%s sval %s" + curr-indent ival diff var sval) + (setq new-val (+ sval diff)) +;;; I commented out this because someone might want to replace +;;; a value of `+' with the current value of sh-basic-offset +;;; or vice-versa. +;;; (if (= 0 diff) +;;; (message "No change needed!") + (sh-set-var-value var new-val no-symbol) + (message "%s set to %s" var (symbol-value var)) + ) + (t + (debug) + (message "Cannot change %s" var)) + )))) + + + +(defun sh-mark-init (buffer) + "Initialize a BUFFER to be used by `sh-mark-line'." + (let ((main-buffer (current-buffer))) + (save-excursion + (set-buffer (get-buffer-create buffer)) + (erase-buffer) + (occur-mode) + (setq occur-buffer main-buffer) + ))) + + +(defun sh-mark-line (message point buffer &optional add-linenum occur-point) + "Insert MESSAGE referring to location POINT in current buffer into BUFFER. +Buffer BUFFER is in `occur-mode'. +If ADD-LINENUM is non-nil the message is preceded by the line number. +If OCCUR-POINT is non-nil then the line is marked as a new occurence +so that `occur-next' and `occur-prev' will work." + (let ((m1 (make-marker)) + (main-buffer (current-buffer)) + start + (line "") ) + (if point + (progn + (set-marker m1 point (current-buffer)) + (if add-linenum + (setq line (format "%d: " (1+ (count-lines 1 point))))))) + (save-excursion + (if (get-buffer buffer) + (set-buffer (get-buffer buffer)) + (set-buffer (get-buffer-create buffer)) + (occur-mode) + (setq occur-buffer main-buffer) + ) + (goto-char (point-max)) + (setq start (point)) + (insert line) + (if occur-point + (setq occur-point (point))) + (insert message) + (if point + (put-text-property start (point) 'mouse-face 'highlight)) + (insert "\n") + (if point + (progn + (put-text-property start (point) 'occur m1) + (if occur-point + (put-text-property occur-point (1+ occur-point) + 'occur-point t)) + )) + ))) + + + +;; Is this really worth having? +(defvar sh-learned-buffer-hook nil + "*An abnormal hook, called with an alist of leared variables.") +;;; Example of how to use sh-learned-buffer-hook +;; +;; (defun what-i-learned (list) +;; (let ((p list)) +;; (save-excursion +;; (set-buffer "*scratch*") +;; (goto-char (point-max)) +;; (insert "(setq\n") +;; (while p +;; (insert (format " %s %s \n" +;; (nth 0 (car p)) (nth 1 (car p)))) +;; (setq p (cdr p))) +;; (insert ")\n") +;; ))) +;; +;; (add-hook 'sh-learned-buffer-hook 'what-i-learned) + + +;; Originally this was sh-learn-region-indent (beg end) +;; However, in practise this was awkward so I changed it to +;; use the whole buffer. Use narrowing if needbe. +(defun sh-learn-buffer-indent (&optional arg) + "Learn how to indent the buffer the way it currently is. + +Output in buffer \"*indent*\" shows any lines which have conflicting +values of a variable, and the final value of all variables learnt. +This buffer is popped to automatically if there are any discrepencies. + +If no prefix ARG is given, then variables are set to numbers. +If a prefix arg is given, then variables are set to symbols when +applicable -- e.g. to symbol `+' if the value is that of the +basic indent. +If a positive numerical prefix is given, then `sh-basic-offset' +is set to the prefix's numerical value. +Otherwise, sh-basic-offset may or may not be changed, according +to the value of variable `sh-learn-basic-offset'. + +Abnormal hook `sh-learned-buffer-hook' if non-nil is called when the +function completes. The function is abnormal because it is called +with an alist of variables learnt. This feature may be changed or +removed in the future. + +This command can often take a long time to run." + (interactive "P") + (sh-must-support-indent) + (save-excursion + (goto-char (point-min)) + (let ((learned-var-list nil) + (out-buffer "*indent*") + (num-diffs 0) + last-pos + previous-set-info + (max 17) + vec + msg + (comment-col nil) ;; number if all same, t if seen diff values + (comments-always-default t) ;; nil if we see one not default + initial-msg + (specified-basic-offset (and arg (numberp arg) + (> arg 0))) + (linenum 0) + suggested) + (setq vec (make-vector max 0)) + (sh-mark-init out-buffer) + + (if specified-basic-offset + (progn + (setq sh-basic-offset arg) + (setq initial-msg + (format "Using specified sh-basic-offset of %d" + sh-basic-offset))) + (setq initial-msg + (format "Initial value of sh-basic-offset: %s" + sh-basic-offset))) + + (while (< (point) (point-max)) + (setq linenum (1+ linenum)) +;; (if (zerop (% linenum 10)) + (message "line %d" linenum) +;; ) + (unless (looking-at "\\s-*$") ;; ignore empty lines! + (let* ((sh-indent-comment t) ;; info must return default indent + (info (sh-get-indent-info)) + (var (sh-get-indent-var-for-line info)) + sval ival diff new-val + (curr-indent (current-indentation))) + (cond + ((null var) + nil) + ((stringp var) + nil) + ((numberp (setq sval (sh-var-value var 'no-error))) + ;; the numberp excludes comments since sval will be t. + (setq ival (sh-calculate-indent)) + (setq diff (- curr-indent ival)) + (setq new-val (+ sval diff)) + (sh-set-var-value var new-val 'no-symbol) + (unless (looking-at "\\s-*#");; don't learn from comments + (if (setq previous-set-info (assoc var learned-var-list)) + (progn + ;; it was already there, is it same value ? + (unless (eq (symbol-value var) + (nth 1 previous-set-info)) + (sh-mark-line + (format "Variable %s was set to %s" + var (symbol-value var)) + (point) out-buffer t t) + (sh-mark-line + (format " but was previously set to %s" + (nth 1 previous-set-info)) + (nth 2 previous-set-info) out-buffer t) + (setq num-diffs (1+ num-diffs)) + ;; (delete previous-set-info learned-var-list) + (setcdr previous-set-info + (list (symbol-value var) (point))) + ) + ) + (setq learned-var-list + (append (list (list var (symbol-value var) + (point))) + learned-var-list))) + (if (numberp new-val) + (progn + (sh-debug + "This line's indent value: %d" new-val) + (if (< new-val 0) + (setq new-val (- new-val))) + (if (< new-val max) + (aset vec new-val (1+ (aref vec new-val)))))) + )) + ((eq var 'sh-indent-comment) + (unless (= curr-indent (sh-calculate-indent info)) + ;; this is not the default indentation + (setq comments-always-default nil) + (if comment-col;; then we have see one before + (or (eq comment-col curr-indent) + (setq comment-col t));; seen a different one + (setq comment-col curr-indent)) + )) + (t + (sh-debug "Cannot learn this line!!!") + )) + (sh-debug + "at %s learned-var-list is %s" (point) learned-var-list) + )) + (forward-line 1) + ) ;; while + (if sh-debug + (progn + (setq msg (format + "comment-col = %s comments-always-default = %s" + comment-col comments-always-default)) + ;; (message msg) + (sh-mark-line msg nil out-buffer))) + (cond + ((eq comment-col 0) + (setq msg "\nComments are all in 1st column.\n")) + (comments-always-default + (setq msg "\nComments follow default indentation.\n") + (setq comment-col t)) + ((numberp comment-col) + (setq msg (format "\nComments are in col %d." comment-col))) + (t + (setq msg "\nComments seem to be mixed, leaving them as is.\n") + (setq comment-col nil) + )) + (sh-debug msg) + (sh-mark-line msg nil out-buffer) + + (sh-mark-line initial-msg nil out-buffer t t) + + (setq suggested (sh-guess-basic-offset vec)) + + (if (and suggested (not specified-basic-offset)) + (let ((new-value + (cond + ;; t => set it if we have a single value as a number + ((and (eq sh-learn-basic-offset t) (numberp suggested)) + suggested) + ;; other non-nil => set it if only one value was found + (sh-learn-basic-offset + (if (numberp suggested) + suggested + (if (= (length suggested) 1) + (car suggested)))) + (t + nil)))) + (if new-value + (progn + (setq learned-var-list + (append (list (list 'sh-basic-offset + (setq sh-basic-offset new-value) + (point-max))) + learned-var-list)) + ;; Not sure if we need to put this line in, since + ;; it will appear in the "Learned variable settings". + (sh-mark-line + (format "Changed sh-basic-offset to: %d" sh-basic-offset) + nil out-buffer)) + (sh-mark-line + (if (listp suggested) + (format "Possible value(s) for sh-basic-offset: %s" + (mapconcat 'int-to-string suggested " ")) + (format "Suggested sh-basic-offset: %d" suggested)) + nil out-buffer)))) + + + (setq learned-var-list + (append (list (list 'sh-indent-comment comment-col (point-max))) + learned-var-list)) + (setq sh-indent-comment comment-col) + (let ((name (buffer-name)) + (lines (if (and (eq (point-min) 1) + (eq (point-max) (1+ (buffer-size)))) + "" + (format "lines %d to %d of " + (1+ (count-lines 1 (point-min))) + (1+ (count-lines 1 (point-max)))))) + ) + (sh-mark-line "\nLearned variable settings:" nil out-buffer) + (if arg + ;; Set learned variables to symbolic rather than numeric + ;; values where possible. + (progn + (let ((p (reverse learned-var-list)) + var val) + (while p + (setq var (car (car p))) + (setq val (nth 1 (car p))) + (cond + ((eq var 'sh-basic-offset) + ) + ((numberp val) + (sh-set-var-value var val)) + (t + )) + (setq p (cdr p)) + )))) + (let ((p (reverse learned-var-list)) + var) + (while p + (setq var (car (car p))) + (sh-mark-line (format " %s %s" var (symbol-value var)) + (nth 2 (car p)) out-buffer) + (setq p (cdr p)))) + (save-excursion + (set-buffer out-buffer) + (goto-char (point-min)) + (insert + (format "Indentation values for buffer %s.\n" name) + (format "%d indentation variable%s different values%s\n\n" + num-diffs + (if (= num-diffs 1) + " has" "s have") + (if (zerop num-diffs) + "." ":")) + ))) + ;; Are abnormal hooks considered bad form? + (run-hook-with-args 'sh-learned-buffer-hook learned-var-list) + (if (or sh-popup-occur-buffer (> num-diffs 0)) + (pop-to-buffer out-buffer)) + ))) + +(defun sh-guess-basic-offset (vec) + "See if we can determine a reasonbable value for `sh-basic-offset'. +This is experimental, heuristic and arbitrary! +Argument VEC is a vector of information collected by +`sh-learn-buffer-indent'. +Return values: + number - there appears to be a good single value + list of numbers - no obvious one, here is a list of one or more + reasonable choices + nil - we couldn't find a reasonable one." + (let* ((max (1- (length vec))) + (i 1) + (totals (make-vector max 0)) + (return nil) + j) + (while (< i max) + (aset totals i (+ (aref totals i) (* 4 (aref vec i)))) + (setq j (/ i 2)) + (if (zerop (% i 2)) + (aset totals i (+ (aref totals i) (aref vec (/ i 2))))) + (if (< (* i 2) max) + (aset totals i (+ (aref totals i) (aref vec (* i 2))))) + (setq i (1+ i)) + ) + (let ((x nil) + (result nil) + tot sum p) + (setq i 1) + (while (< i max) + (if (/= (aref totals i) 0) + (setq x (append x (list (cons i (aref totals i)))))) + (setq i (1+ i))) + + (setq x (sort x '(lambda (a b) + (> (cdr a)(cdr b))))) + (setq tot (apply '+ (append totals nil))) + (sh-debug (format "vec: %s\ntotals: %s\ntot: %d" + vec totals tot)) + (cond + ((zerop (length x)) + (message "no values!")) ;; we return nil + ((= (length x) 1) + (message "only value is %d" (car (car x))) + (setq result (car (car x)))) ;; return single value + ((> (cdr (car x)) (/ tot 2)) + ;; 1st is > 50% + (message "basic-offset is probably %d" (car (car x))) + (setq result (car (car x)))) ;; again, return a single value + ((>= (cdr (car x)) (* 2 (cdr (car (cdr x))))) + ;; 1st is >= 2 * 2nd + (message "basic-offset could be %d" (car (car x))) + (setq result (car (car x)))) + ((>= (+ (cdr (car x))(cdr (car (cdr x)))) (/ tot 2)) + ;; 1st & 2nd together >= 50% - return a list + (setq p x sum 0 result nil) + (while (and p + (<= (setq sum (+ sum (cdr (car p)))) (/ tot 2))) + (setq result (append result (list (car (car p))))) + (setq p (cdr p))) + (message "Possible choices for sh-basic-offset: %s" + (mapconcat 'int-to-string result " "))) + (t + (message "No obvious value for sh-basic-offset. Perhaps %d" + (car (car x))) + ;; result is nil here + )) + result + ))) + + +(defun sh-do-nothing (a b c) + ;; checkdoc-params: (a b c) + "A dummy function to prevent font-lock from re-fontifying a change. +Otherwise, we fontify something and font-lock overwrites it." + ) + +(defun sh-set-char-syntax (where new-prop) + "Set the character's syntax table property at WHERE to be NEW-PROP." + (or where + (setq where (point))) + (let ((font-lock-fontify-region-function 'sh-do-nothing)) + (put-text-property where (1+ where) 'syntax-table new-prop) + (add-text-properties where (1+ where) + '(face sh-st-face rear-nonsticky t)) + )) + + +(defun sh-check-paren-in-case () + "Make syntax class of case label's right parenthesis not close parenthesis. +If this parenthesis is a case alternative, set its syntax class to a word." + (let ((start (point)) + state prev-line) + ;; First test if this is a possible candidate, the first "(" or ")" + ;; on the line; then, if go, check prev line is ;; or case. + (save-excursion + (beginning-of-line) + ;; stop at comment or when depth becomes -1 + (setq state (parse-partial-sexp (point) start -1 nil nil t)) + (if (and + (= (car state) -1) + (= (point) start) + (setq prev-line (sh-prev-line nil))) + (progn + (goto-char prev-line) + (beginning-of-line) + ;; (setq case-stmt-start (point)) + ;; (if (looking-at "\\(^\\s-*case[^-a-z0-9_]\\|[^#]*;;\\s-*$\\)") + (if (sh-search-word "\\(case\\|;;\\)" start) + (sh-set-char-syntax (1- start) sh-special-syntax) + )))))) + +(defun sh-electric-rparen () + "Insert a right parethese, and check if it is a case alternative. +If so, its syntax class is set to word, and its text proerty +is set to have face `sh-st-face'." + (interactive) + (insert ")") + (if sh-electric-rparen-needed-here + (sh-check-paren-in-case))) + +(defun sh-electric-hash () + "Insert a hash, but check it is preceded by \"$\". +If so, it is given a syntax type of comment. +Its text proerty has face `sh-st-face'." + (interactive) + (let ((pos (point))) + (insert "#") + (if (eq (char-before pos) ?$) + (sh-set-char-syntax pos sh-st-punc)))) + +(defun sh-electric-less (arg) + "Insert a \"<\" and see if this is the start of a here-document. +If so, the syntax class is set so that it will not be automatically +reindented. +Argument ARG if non-nil disables this test." + (interactive "*P") + (let ((p1 (point)) p2 p3) + (sh-maybe-here-document arg) ;; call the original fn in sh-script.el. + (setq p2 (point)) + (if (/= (+ p1 (prefix-numeric-value arg)) p2) + (save-excursion + (forward-line 1) + (end-of-line) + (setq p3 (point)) + (sh-set-here-doc-region p2 p3)) + ))) + +(defun sh-set-here-doc-region (start end) + "Mark a here-document from START to END so that it will not be reindented." + (interactive "r") + ;; Make the whole thing have syntax type word... + ;; That way sexp movement doens't worry about any parentheses. + ;; A disadvantage of this is we can't use forward-word within a + ;; here-doc, which is annoying. + (let ((font-lock-fontify-region-function 'sh-do-nothing)) + (put-text-property start end 'syntax-table sh-here-doc-syntax) + (put-text-property start end 'face 'sh-heredoc-face) + (put-text-property (1- end) end 'rear-nonsticky t) + (put-text-property start (1+ start) 'front-sticky t) + )) + +(defun sh-remove-our-text-properties () + "Remove text properties relating to right parentheses and here documents." + (interactive) + (save-excursion + (goto-char (point-min)) + (while (not (eobp)) + (let ((plist (text-properties-at (point))) + (next-change + (or (next-single-property-change (point) 'syntax-table + (current-buffer) ) + (point-max)))) + ;; Process text from point to NEXT-CHANGE... + (if (get-text-property (point) 'syntax-table) + (progn + (sh-debug "-- removing props from %d to %d --" + (point) next-change) + (remove-text-properties (point) next-change + '(syntax-table nil)) + (remove-text-properties (point) next-change '(face nil)) + )) + (goto-char next-change))) + )) + +(defun sh-search-word (word &optional limit) + "Search forward for regexp WORD occuring as a word not in string nor comment. +If found, returns non nil with the match available in \(match-string 2\). +Yes 2, not 1, since we build a regexp to guard against false matches +such as matching \"a-case\" when we are searching for \"case\". +If not found, it returns nil. +The search maybe limited by optional argument LIMIT." + (interactive "sSearch for: ") + (let ((found nil) + ;; Cannot use \\b here since it matches "-" and "_" + (regexp (sh-mkword-regexp word)) + start state where) + (setq start (point)) + (while (and (setq start (point)) + (not found) + (re-search-forward regexp limit t)) + ;; Found str; check it is not in a comment or string. + (setq state + ;; Stop on comment: + (parse-partial-sexp start (point) nil nil nil 'syntax_table)) + (if (setq where (nth 8 state)) + ;; in comment or string + (if (= where -1) + (setq found (point)) + (if (eq (char-after where) ?#) + (end-of-line) + (goto-char where) + (unless (sh-safe-forward-sexp) + ;; If the above fails we must either give up or + ;; move forward and try again. + (forward-line 1)) + )) + ;; not in comment or string, so accept it + (setq found (point)) + )) + found + )) + +(defun sh-scan-case () + "Scan a case statement for right parens belonging to case alternatives. +Mark each as having syntax `sh-special-syntax'. +Called from scan-buff. If ok, return non-nil." + (let (end + state + (depth 1) ;; we are called at a "case" + (start (point)) + (return t)) + ;; We enter here at a case statement + ;; First, find limits of the case. + (while (and (> depth 0) + (sh-search-word "\\(case\\|esac\\)")) + (if (equal (match-string 2) "case") + (setq depth (1+ depth)) + (setq depth (1- depth)))) + ;; (message "end of search for esac at %d depth=%d" (point) depth) + (setq end (point)) + (goto-char start) + ;; if we found the esac, then fix all appropriate ')'s in the region + (if (zerop depth) + (progn + (while (< (point) end) + ;; search for targetdepth of -1 meaning extra right paren + (setq state (parse-partial-sexp (point) end -1 nil nil nil)) + (if (and (= (car state) -1) + (= (char-before) ?\))) + (progn + ;; (message "At %d state is %s" (point) state) + ;; (message "Fixing %d" (point)) + (sh-set-char-syntax (1- (point)) sh-special-syntax) + ;; we could advance to the next ";;" perhaps + ) + ;; (message "? Not found at %d" (point)) ; ok, could be "]" + )) + (goto-char end)) + (message "No matching esac for case at %d" start) + (setq return nil) + ) + return + )) + + +(defun sh-scan-buffer () + "Scan a sh buffer for case statements and here-documents. + +For each case alternative found, mark its \")\" with a text property +so that its syntax class is no longer a close parenthesis character. + +Each here-document is also marked so that it is effectively immune +from indenation changes." + ;; Do not call this interactively, call `sh-rescan-buffer' instead. + (sh-must-be-shell-mode) + (let ((n 0) + (initial-buffer-modified-p (buffer-modified-p)) + start end where label ws) + (save-excursion + (goto-char (point-min)) + ;; 1. Scan for ")" in case statements. + (while (and ;; (re-search-forward "^[^#]*\\bcase\\b" nil t) + (sh-search-word "\\(case\\|esac\\)") + ;; (progn (message "Found a case at %d" (point)) t) + (sh-scan-case))) + ;; 2. Scan for here docs + (goto-char (point-min)) + ;; while (re-search-forward "<<\\(-?\\)\\(\\s-*\\)\\(.*\\)$" nil t) + (while (re-search-forward "<<\\(-?\\)" nil t) + (unless (sh-in-comment-or-string (match-beginning 0)) + ;; (setq label (match-string 3)) + (setq label (sh-get-word)) + (if (string= (match-string 1) "-") + ;; if <<- then we allow whitespace + (setq ws "\\s-*") + ;; otherwise we don't + (setq ws "")) + (while (string-match "['\"\\]" label) + (setq label (replace-match "" nil nil label))) + (if (setq n (string-match "\\s-+$" label)) + (setq label (substring label 0 n))) + (forward-line 1) + ;; the line containing the << could be continued... + (while (sh-this-is-a-continuation) + (forward-line 1)) + (setq start (point)) + (if (re-search-forward (concat "^" ws (regexp-quote label) + "\\s-*$") + nil t) + (sh-set-here-doc-region start (point)) + (sh-debug "missing here-doc delimiter `%s'" label)))) + ;; 3. Scan for $# -- make the "#" a punctuation not a comment + (goto-char (point-min)) + (let (state) + (while (and (not (eobp)) + (setq state (parse-partial-sexp + (1+ (point))(point-max) nil nil nil t)) + (nth 4 state)) + (goto-char (nth 8 state)) + (sh-debug "At %d %s" (point) (eq (char-before) ?$)) + (if (eq (char-before) ?$) + (sh-set-char-syntax (point) sh-st-punc) ;; not a comment! + (end-of-line) ;; if this *was* a comment, ignore rest of line! + ))) + ;; 4. Hide these changes from making a previously unmodified + ;; buffer into a modified buffer. + (if sh-debug + (if initial-buffer-modified-p + (message "buffer was initially modified") + (message + "buffer not initially modified - so clearing modified flag"))) + (set-buffer-modified-p initial-buffer-modified-p) + ))) + +(defun sh-rescan-buffer () + "Rescan the buffer for case alternative parentheses and here documents." + (interactive) + (if (eq major-mode 'sh-mode) + (let ((inhibit-read-only t)) + (sh-remove-our-text-properties) + (message "Re-scanning buffer...") + (sh-scan-buffer) + (message "Re-scanning buffer...done") + ))) + +;; ======================================================================== + +;; Styles -- a quick and dirty way of saving the indenation settings. + +(defvar sh-styles-alist nil + "A list of all known shell indentation styles.") + +(defun sh-name-style (name &optional confirm-overwrite) + "Name the current indentation settings as a style called NAME. +If this name exists, the command will prompt whether it should be +overwritten if +- - it was called interactively with a prefix argument, or +- - called non-interactively with optional CONFIRM-OVERWRITE non-nil." + ;; (interactive "sName for this style: ") + (interactive + (list + (read-from-minibuffer "Name for this style? " ) + (not current-prefix-arg))) + (let ((slist (list name)) + (p sh-var-list) + var style) + (while p + (setq var (car p)) + (setq slist (append slist (list (cons var (symbol-value var))))) + (setq p (cdr p))) + (if (setq style (assoc name sh-styles-alist)) + (if (or (not confirm-overwrite) + (y-or-n-p "This style exists. Overwrite it? ")) + (progn + (message "Updating style %s" name) + (setcdr style (cdr slist))) + (message "Not changing style %s" name)) + (message "Creating new style %s" name) + (setq sh-styles-alist (append sh-styles-alist + (list slist))) + ))) + +(defun sh-load-style (name) + "Set shell indentation values for this buffer from those in style NAME." + (interactive (list (completing-read + "Which style to use for this buffer? " + sh-styles-alist nil t))) + (let ((sl (assoc name sh-styles-alist))) + (if (null sl) + (error "sh-load-style - style %s not known" name) + (setq sl (cdr sl)) + (while sl + (set (car (car sl)) (cdr (car sl))) + (setq sl (cdr sl)) + )))) + +(defun sh-save-styles-to-buffer (buff) + "Save all current styles in elisp to buffer BUFF. +This is always added to the end of the buffer." + (interactive (list + (read-from-minibuffer "Buffer to save styles in? " "*scratch*"))) + ;; This is an attempt to sort of pretty print it... + (save-excursion + (set-buffer (get-buffer-create buff)) + (goto-char (point-max)) + (insert "\n") + (let ((p sh-styles-alist) q) + (insert "(setq sh-styles-alist '(\n") + (while p + (setq q (car p)) + (insert " ( " (prin1-to-string (car q)) "\n") + (setq q (cdr q)) + (while q + (insert " "(prin1-to-string (car q)) "\n") + (setq q (cdr q))) + (insert " )\n") + (setq p (cdr p)) + ) + (insert "))\n") + ))) + + + + ;; statement syntax-commands for various shells ;; You are welcome to add the syntax or even completely new statements as @@ -1083,47 +3565,52 @@ < < "endsw") (es) (rc "expression: " - "switch( " str " ) {" \n + > "switch( " str " ) {" \n > "case " (read-string "pattern: ") \n > _ \n ( "other pattern, %s: " - < "case " str \n + "case " str > \n > _ \n) - < "case *" \n + "case *" > \n > _ \n resume: - < < ?}) + ?} > ) (sh "expression: " - "case " str " in" \n - > (read-string "pattern: ") ?\) \n + > "case " str " in" \n + > (read-string "pattern: ") + '(sh-electric-rparen) + \n > _ \n ";;" \n ( "other pattern, %s: " - < str ?\) \n + > str '(sh-electric-rparen) \n > _ \n ";;" \n) - < "*)" \n + > "*" '(sh-electric-rparen) \n > _ \n resume: - < < "esac")) + "esac" > )) (define-skeleton sh-for "Insert a for loop. See `sh-feature'." (csh eval sh-modify sh - 1 "foreach " - 3 " ( " - 5 " )" - 15 "end") + 1 "" + 2 "foreach " + 4 " ( " + 6 " )" + 15 '< + 16 "end" + ) (es eval sh-modify rc - 3 " = ") + 4 " = ") (rc eval sh-modify sh - 1 "for( " - 5 " ) {" - 15 ?}) + 2 "for( " + 6 " ) {" + 15 ?} ) (sh "Index variable: " - "for " str " in " _ "; do" \n + > "for " str " in " _ "; do" \n > _ | ?$ & (sh-remember-variable str) \n - < "done")) + "done" > )) @@ -1137,34 +3624,34 @@ "@ " str "++" \n < "end") (es eval sh-modify rc - 3 " =") + 4 " =") (ksh88 "Index variable: " - "integer " str "=0" \n - "while (( ( " str " += 1 ) <= " + > "integer " str "=0" \n + > "while (( ( " str " += 1 ) <= " (read-string "upper limit: ") " )); do" \n - > _ ?$ (sh-remember-variable str) \n - < "done") + > _ ?$ (sh-remember-variable str) > \n + "done" > ) (posix "Index variable: " - str "=1" \n + > str "=1" \n "while [ $" str " -le " (read-string "upper limit: ") " ]; do" \n > _ ?$ str \n str ?= (sh-add (sh-remember-variable str) 1) \n - < "done") + "done" > ) (rc "Index variable: " - "for( " str " in" " `{awk 'BEGIN { for( i=1; i<=" + > "for( " str " in" " `{awk 'BEGIN { for( i=1; i<=" (read-string "upper limit: ") - "; i++ ) print i }'}) {" \n + "; i++ ) print i }'`}) {" \n > _ ?$ (sh-remember-variable str) \n - < ?}) + ?} >) (sh "Index variable: " - "for " str " in `awk 'BEGIN { for( i=1; i<=" + > "for " str " in `awk 'BEGIN { for( i=1; i<=" (read-string "upper limit: ") "; i++ ) print i }'`; do" \n > _ ?$ (sh-remember-variable str) \n - < "done")) + "done" > )) (defun sh-shell-initialize-variables () @@ -1264,46 +3751,51 @@ resume: < "endif") (es "condition: " - "if { " str " } {" \n + > "if { " str " } {" \n > _ \n ( "other condition, %s: " - < "} { " str " } {" \n + "} { " str " } {" > \n > _ \n) - < "} {" \n + "} {" > \n > _ \n resume: - < ?}) - (rc eval sh-modify csh - 3 " ) {" - 8 '( "other condition, %s: " - < "} else if( " str " ) {" \n + ?} > ) + (rc "condition: " + > "if( " str " ) {" \n + > _ \n + ( "other condition, %s: " + "} else if( " str " ) {" > \n > _ \n) - 10 "} else {" - 17 ?}) + "} else {" > \n + > _ \n + resume: + ?} > + ) (sh "condition: " '(setq input (sh-feature sh-test)) - "if " str "; then" \n + > "if " str "; then" \n > _ \n ( "other condition, %s: " - < "elif " str "; then" \n - > _ \n) - < "else" \n - > _ \n + > "elif " str "; then" > \n + > \n) + "else" > \n + > \n resume: - < "fi")) + "fi" > )) (define-skeleton sh-repeat "Insert a repeat loop definition. See `sh-feature'." (es nil - "forever {" \n + > "forever {" \n > _ \n - < ?}) + ?} > ) (zsh "factor: " - "repeat " str "; do"\n - > _ \n - < "done")) + > "repeat " str "; do" > \n + > \n + "done" > )) + ;;;(put 'sh-repeat 'menu-enable '(sh-feature sh-repeat)) @@ -1311,9 +3803,11 @@ (define-skeleton sh-select "Insert a select statement. See `sh-feature'." (ksh88 "Index variable: " - "select " str " in " _ "; do" \n + > "select " str " in " _ "; do" \n > ?$ str \n - < "done")) + "done" > ) + (bash eval sh-append ksh88) + ) ;;;(put 'sh-select 'menu-enable '(sh-feature sh-select)) @@ -1330,21 +3824,22 @@ "exit:\n" "rm $tmp* >&/dev/null" >) (es (file-name-nondirectory (buffer-file-name)) - "local( signals = $signals sighup sigint; tmp = /tmp/" str ".$pid ) {" \n + > "local( signals = $signals sighup sigint; tmp = /tmp/" str + ".$pid ) {" \n > "catch @ e {" \n > "rm $tmp^* >[2]/dev/null" \n "throw $e" \n - < "} {" \n - > _ \n - < ?} \n - < ?}) + "} {" > \n + _ \n + ?} > \n + ?} > ) (ksh88 eval sh-modify sh - 6 "EXIT") + 7 "EXIT") (rc (file-name-nondirectory (buffer-file-name)) - "tmp = /tmp/" str ".$pid" \n + > "tmp = /tmp/" str ".$pid" \n "fn sigexit { rm $tmp^* >[2]/dev/null }") (sh (file-name-nondirectory (buffer-file-name)) - "TMP=${TMPDIR:-/tmp}/" str ".$$" \n + > "TMP=${TMPDIR:-/tmp}/" str ".$$" \n "trap \"rm $TMP* 2>/dev/null\" " ?0)) @@ -1353,9 +3848,9 @@ "Insert an until loop. See `sh-feature'." (sh "condition: " '(setq input (sh-feature sh-test)) - "until " str "; do" \n + > "until " str "; do" \n > _ \n - < "done")) + "done" > )) ;;;(put 'sh-until 'menu-enable '(sh-feature sh-until)) @@ -1363,20 +3858,24 @@ (define-skeleton sh-while "Insert a while loop. See `sh-feature'." (csh eval sh-modify sh - 2 "while( " - 4 " )" - 10 "end") - (es eval sh-modify rc - 2 "while { " - 4 " } {") - (rc eval sh-modify csh - 4 " ) {" - 10 ?}) + 2 "" + 3 "while( " + 5 " )" + 10 '< + 11 "end" ) + (es eval sh-modify sh + 3 "while { " + 5 " } {" + 10 ?} ) + (rc eval sh-modify sh + 3 "while( " + 5 " ) {" + 10 ?} ) (sh "condition: " '(setq input (sh-feature sh-test)) - "while " str "; do" \n + > "while " str "; do" \n > _ \n - < "done")) + "done" > )) @@ -1416,9 +3915,8 @@ (posix eval sh-modify sh 18 "$(basename $0)") (sh "optstring: " - "while getopts :" str " OPT; do" \n + > "while getopts :" str " OPT; do" \n > "case $OPT in" \n - > > '(setq v1 (append (vconcat str) nil)) ( (prog1 (if v1 (char-to-string (car v1))) (if (eq (nth 1 v1) ?:) @@ -1426,10 +3924,10 @@ v2 "\"$OPTARG\"") (setq v1 (cdr v1) v2 nil))) - < str "|+" str ?\) \n + > str "|+" str '(sh-electric-rparen) \n > _ v2 \n - ";;" \n) - < "*)" \n + > ";;" \n) + > "*" '(sh-electric-rparen) \n > "echo" " \"usage: " "`basename $0`" " [+-" '(setq v1 (point)) str '(save-excursion @@ -1437,9 +3935,10 @@ (replace-match " ARG] [+-" t t))) (if (eq (preceding-char) ?-) -5) "] [--] ARGS...\"" \n - "exit 2" \n - < < "esac" \n - < "done" \n + "exit 2" > \n + "esac" > + \n "done" + > \n "shift " (sh-add "OPTIND" -1))) @@ -1508,15 +4007,12 @@ (point))) (newline)))) - - (defun sh-beginning-of-command () "Move point to successive beginnings of commands." (interactive) (if (re-search-backward sh-beginning-of-command nil t) (goto-char (match-beginning 2)))) - (defun sh-end-of-command () "Move point to successive ends of commands." (interactive) @@ -1525,4 +4021,4 @@ (provide 'sh-script) -;; sh-script.el ends here +;;; sh-script.el ends here