branch: externals/sql-indent
commit 45f8e15a0f34afc54691ca54c4c34edcd987e141
Author: Alex Harsanyi <[email protected]>
Commit: Alex Harsanyi <[email protected]>
initial revision
---
sql-indent.el | 1668 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 1668 insertions(+)
diff --git a/sql-indent.el b/sql-indent.el
new file mode 100644
index 0000000..d404c65
--- /dev/null
+++ b/sql-indent.el
@@ -0,0 +1,1668 @@
+;; sql-indent.el -- Support for indenting code in SQL files.
+;;
+;; Copyright (C) 2015 Alex Harsanyi
+;;
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 2 of the License, or
+;; (at your option) any later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software
+;; Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+;;
+
+;; Author: Alex Harsanyi ([email protected])
+;; Created: 27 Sep 2006 (public release 21 Dec 2015)
+;; Keywords: languages sql
+
+;;; Commentary
+;;
+;; To use this mode, byte compile this file, than add the following to
+;; your ~/.emacs.el:
+;;
+;; (require 'sql-indent)
+;; (add-hook 'sql-mode-hook 'sqlind-setup)
+;;
+;; To adjust the indentation, see `sqlind-basic-offset' and
+;; `sqlind-indentation-offsets-alist' variables.
+;;
+
+(require 'sql)
+(eval-when-compile (require 'cc-defs)) ; for c-point
+
+;;;; General setup
+
+(defvar sqlind-syntax-table
+ (let ((table (make-syntax-table)))
+ ;; C-style comments /**/ (see elisp manual "Syntax Flags"))
+ (modify-syntax-entry ?/ ". 14" table)
+ (modify-syntax-entry ?* ". 23" table)
+ ;; double-dash starts comment
+ (modify-syntax-entry ?- ". 12b" table)
+ ;; newline and formfeed end coments
+ (modify-syntax-entry ?\n "> b" table)
+ (modify-syntax-entry ?\f "> b" table)
+ ;; single quotes (') quotes delimit strings
+ (modify-syntax-entry ?' "\"" table)
+ ;; backslash is no escape character
+ (modify-syntax-entry ?\\ "." table)
+
+ ;; the following are symbol constituents. Note that the dot '.' is more
+ ;; usefull as a symbol constituent than as a punctuation char.
+
+ (modify-syntax-entry ?_ "_" table)
+ (modify-syntax-entry ?. "_" table)
+ (modify-syntax-entry ?$ "_" table)
+ (modify-syntax-entry ?# "_" table)
+ (modify-syntax-entry ?% "_" table)
+
+ table)
+ "Syntax table used in `sql-mode' for editing SQL code.")
+
+
+;;;; Syntactic analysis of SQL code
+
+;;;;; Commentary
+
+;; The following routines perform rudimentary syntactical analysis of SQL
+;; code. The indentation engine decides how to indent based on what this code
+;; returns. The main function is `sqlind-syntax-of-line'.
+;;
+;; To examine the syntax of the current line, you can use the
+;; `sqlind-show-syntax-of-line'. This is only useful if you want to debug this
+;; package or are just curious.
+
+;;;;; Utilities
+
+(defconst sqlind-comment-start-skip "\\(--+\\|/\\*+\\)\\s *"
+ "Regexp to match the start of a SQL comment.")
+
+(defconst sqlind-comment-end "\\*+\\/"
+ "Regexp to match the end of a multiline SQL comment.")
+
+(defvar sqlind-comment-prefix "\\*+\\s "
+ "Regexp to match the line prefix inside comments.
+This is used to indent multi-line comments.")
+
+(defsubst sqlind-in-comment-or-string (pos)
+ "Return non nil if POS is inside a comment or a string.
+We actually return 'string if POS is inside a string, 'comment if
+POS is inside a comment, nil otherwise."
+ (syntax-ppss-context (syntax-ppss pos)))
+
+(defun sqlind-backward-syntactic-ws ()
+ "Move point backwards over whitespace and comments.
+Leave point on the first character which is not syntactic
+whitespace, or at the beginning of the buffer."
+ (catch 'done
+ (while t
+ (skip-chars-backward " \t\n\r\f\v")
+ (unless (eq (point) (point-min))
+ (forward-char -1))
+ (let ((pps (syntax-ppss (point))))
+ (if (nth 4 pps) ; inside a comment?
+ (goto-char (nth 8 pps)) ; move to comment start than repeat
+ (throw 'done (point)) ; done
+ )))))
+
+(defun sqlind-forward-syntactic-ws ()
+ "Move point forward over whitespace and comments.
+Leave point at the first character which is not syntactic
+whitespace, or at the end of the buffer."
+ ;; if we are inside a comment, move to the comment start and scan
+ ;; from there.
+ (let ((pps (syntax-ppss (point))))
+ (when (nth 4 pps)
+ (goto-char (nth 8 pps))))
+ (catch 'done
+ (while t
+ (skip-chars-forward " \t\n\r\f\v")
+ (cond ((looking-at sqlind-comment-start-skip) (forward-comment 1))
+ ;; a slash ("/") by itself is a SQL*plus directive and
+ ;; counts as whitespace
+ ((looking-at "/\\s *$") (goto-char (match-end 0)))
+ (t (throw 'done (point)))))))
+
+(defun sqlind-match-string (pos)
+ "Return the match data at POS in the current buffer.
+This is similar to `match-data', but the text is fetched without
+text properties and it is conveted to lower case."
+ (let ((start (match-beginning pos))
+ (end (match-end pos)))
+ (when (and start end)
+ (downcase (buffer-substring-no-properties start end)))))
+
+(defsubst sqlind-labels-match (lb1 lb2)
+ "Return t when LB1 equals LB2 or LB1 is an empty string.
+This is used to compare start/end block labels where the end
+block label might be empty."
+ (or (string= lb1 lb2)
+ (string= lb1 "")))
+
+(defun sqlind-same-level-statement (point start)
+ "Return t if POINT is at the same syntactic level as START.
+This means that POINT is at the same nesting level and not inside
+a strinf or comment."
+ (let ((parse-info (parse-partial-sexp start point)))
+ (not (or (nth 3 parse-info) ; inside a string
+ (nth 4 parse-info) ; inside a comment
+ (> (nth 0 parse-info) 0))))) ; inside a nested paren
+
+;;;;; Find the beginning of the current statement
+
+(defconst sqlind-sqlplus-directive
+ (concat "^"
+ (regexp-opt '("column" "set" "rem" "define" "spool") t)
+ "\\b")
+ "Match an SQL*Plus directive at the beginning of a line.
+A directive always stands on a line by itself -- we use that to
+determine the statement start in SQL*Plus scripts.
+
+NOTE: we don't allow spaces at the beginning of line, as that
+would conflict with the 'set' keyword in an update statement.")
+
+(defconst sqlind-sqlite-directive
+ "^[.].*?\\>"
+ "Match an SQLite directive at the beginning of a line.
+A directive always stands on a line by itself -- we use that to
+determine the statement start in SQLite scripts.
+
+NOTE: we don't allow spaces at the beginning of line, as that
+would conflict with the 'set' keyword in an update statement.")
+
+(defconst sqlind-ms-directive
+ (concat "^"
+ (regexp-opt '("declare") t)
+ "\\>")
+ "Match an MS SQL Sever directive at the beginning of a line.")
+
+(defun sqlind-begining-of-directive ()
+ "Return the position of an SQL directive, or nil.
+We will never move past one of these in our scan. We also assume
+they are one-line only directives."
+ (when (boundp 'sql-product)
+ (let ((rx (case sql-product
+ (ms sqlind-ms-directive)
+ (sqlite sqlind-sqlite-directive)
+ (oracle sqlind-sqlplus-directive)
+ (t nil))))
+ (when rx
+ (save-excursion
+ (when (re-search-backward rx nil 'noerror)
+ (forward-line 1)
+ (point)))))))
+
+(defun sqlind-beginning-of-statement-1 (limit)
+ "Return the position of a block start, or nil.
+But don't go before LIMIT."
+ (save-excursion
+ (catch 'done
+ (while (not (eq (point) (or limit (point-min))))
+ (when (re-search-backward
+ ";\\|begin\\b\\|loop\\b\\|then\\b\\|else\\b\\|)"
+ limit 'noerror)
+ (let ((candidate-pos (match-end 0)))
+ (cond ((looking-at ")")
+ ;; Skip parenthesis expressions, we don't want to find one
+ ;; of the keywords inside one of them and think this is a
+ ;; statement start.
+ (progn (forward-char 1) (forward-sexp -1)))
+ ((looking-at "\\bthen\\|else\\b")
+ ;; then and else start statements when they are inside
+ ;; blocks, not expressions.
+ (sqlind-backward-syntactic-ws)
+ (when (looking-at ";")
+ (throw 'done candidate-pos)))
+ ((not (sqlind-in-comment-or-string (point)))
+ (throw 'done candidate-pos)))))))))
+
+(defun sqlind-beginning-of-statement ()
+ "Move point to the beginning of the current statement."
+ (interactive)
+ (let* ((directive-start (sqlind-begining-of-directive))
+ (statement-start
+ (or
+ ;; If we are inside a paranthesis expression, the start is the
+ ;; start of that expression.
+ (let ((ppss (syntax-ppss (point))))
+ (when (> (nth 0 ppss) 0)
+ (nth 1 ppss)))
+ ;; Look for an ordinary statement start
+ (sqlind-beginning-of-statement-1 directive-start)
+ ;; Fall back on the directive position...
+ directive-start
+ ;; ... or point-min
+ (point-min))))
+
+ (goto-char statement-start)
+
+ ;; a loop keyword might be part of an "end loop" statement, in that case,
+ ;; the statement starts with the "end" keyword. we use
+ ;; skip-syntax-backward so we won't skip over a semicolon (;)
+ (skip-syntax-backward "w")
+ (when (looking-at "loop")
+ (forward-word -1)
+ (if (looking-at "\\bend\\b")
+ (setq statement-start (match-beginning 0))))
+
+ ;; now skip over any whitespace and comments until we find the
+ ;; first character that is program code.
+ (goto-char statement-start)
+ (sqlind-forward-syntactic-ws)))
+
+;;;;; Find the syntax and beginning of the current block
+
+(defconst sqlind-end-statement-regexp
+ "end\\_>\\(?:[ \n\r\t]*\\)\\(if\\_>\\|loop\\_>\\|case\\_>\\)?\\(?:[
\n\r\f]*\\)\\([a-z0-9_]+\\)?\\(?:[ \n\r\f]*\\);"
+ "Match an end of statement.")
+
+(defvar sqlind-end-stmt-stack nil
+ "Stack of end-of-statement positions.
+This is used by the sqlind-maybe-* functions to skip over SQL
+programming blocks. This variable is dynamically bound.")
+
+(defun sqlind-maybe-end-statement ()
+ "Look for SQL end statements return t if found one.
+If (point) is at an end statement, add it to the
+`sqlind-end-stmt-stack' and return t, otherwise return nil.
+
+See also `sqlind-beginning-of-block'"
+ (when (looking-at sqlind-end-statement-regexp)
+ (prog1 t ; make sure we return t
+ (let ((kind (or (sqlind-match-string 1) ""))
+ (label (or (sqlind-match-string 2) "")))
+ (push (list (point) (if (equal kind "") nil (intern kind)) label)
+ sqlind-end-stmt-stack)))))
+
+(defun sqlind-maybe-then-statement ()
+ "When (point) is on a THEN statement, find the corresponding
+start of the block and report its syntax. This code will skip
+over nested statements to determine the actual start.
+
+Only keywords in program code are matched, not the ones inside
+expressions.
+
+See also `sqlind-beginning-of-block'"
+ (when (looking-at "then")
+ (prog1 t ; make sure we return t
+ (let ((start-pos (point)))
+
+ ;; a then keyword at the begining of a line is a block start
+ (when (null sqlind-end-stmt-stack)
+ (save-excursion
+ (back-to-indentation)
+ (when (eq (point) start-pos)
+ (throw 'finished (list 'in-block 'then "")))))
+
+ ;; if it is not at the beginning of a line, the beginning of
+ ;; the statement is the block start
+ (ignore-errors (forward-char -1))
+ (sqlind-beginning-of-statement)
+ ;; a then keyword only starts a block when it is part of an
+ ;; if, case/when or exception statement
+ (cond
+ ((looking-at "\\(<<[a-z0-9_]+>>\\)?\\(?:[
\n\r\f]*\\)\\(\\(?:els\\)?if\\)\\_>")
+ (let ((if-label (sqlind-match-string 1))
+ (if-kind (intern (sqlind-match-string 2)))) ; can be if or
elsif
+ (setq if-label (if if-label (substring if-label 2 -2) ""))
+ (if (null sqlind-end-stmt-stack)
+ (throw 'finished (list 'in-block if-kind if-label))
+ ;; "if" blocks (but not "elsif" blocks) need to be
+ ;; ended with "end if"
+ (when (eq if-kind 'if)
+ (destructuring-bind (pos kind label)
+ (pop sqlind-end-stmt-stack)
+ (unless (and (eq kind 'if)
+ (sqlind-labels-match label if-label))
+ (throw 'finished
+ (list 'syntax-error
+ "bad closing for if block" (point) pos))))))))
+ ((looking-at "\\(<<[a-z0-9_]+>>\\)?\\(?:[ \n\r\f]*\\)case\\_>")
+ ;; find the nearest when block, but only if there are no
+ ;; end statements in the stack
+ (let ((case-label (sqlind-match-string 1)))
+ (setq case-label
+ (if case-label (substring case-label 2 -2) ""))
+ (if (null sqlind-end-stmt-stack)
+ (let ((limit (point)))
+ (goto-char start-pos)
+ (while (re-search-backward "\\_<when\\_>" limit 'noerror)
+ (unless (sqlind-in-comment-or-string (point))
+ (throw 'finished
+ (list 'in-block 'case case-label)))))
+ ;; else
+ (destructuring-bind (pos kind label)
+ (pop sqlind-end-stmt-stack)
+ (unless (and (eq kind 'case)
+ (sqlind-labels-match label case-label))
+ (throw 'finished
+ (list 'syntax-error
+ "bad closing for case block" (point) pos)))))))
+ ((looking-at "exception\\_>")
+ ;; an exception statement is a block start only if we have
+ ;; no end statements in the stack
+ (when (null sqlind-end-stmt-stack)
+ (throw 'finished (list 'in-block 'exception "")))))))))
+
+(defun sqlind-maybe-else-statement ()
+ "If (point) is on an ELSE statement, report its syntax.
+Only keywords in program code are matched, not the ones inside
+expressions.
+
+See also `sqlind-beginning-of-block'"
+ ;; an else statement is only a block start if the `sqlind-end-stmt-stack' is
+ ;; empty, otherwise, we don't do anything.
+ (when (and (looking-at "els\\(e\\|if\\)")
+ (null sqlind-end-stmt-stack))
+ (throw 'finished (list 'in-block (intern (sqlind-match-string 0)) ""))))
+
+(defun sqlind-maybe-loop-statement ()
+ "If (point) is on a LOOP statement, report its syntax.
+Only keywords in program code are matched, not the ones inside
+expressions.
+
+See also `sqlind-beginning-of-block'"
+ (when (looking-at "loop")
+ (prog1 t ; make sure we return t
+ (sqlind-beginning-of-statement)
+ ;; note that we might have found a loop in an "end loop;" statement.
+ (or (sqlind-maybe-end-statement)
+ (progn
+ (let ((loop-label (if (looking-at "<<\\([a-z0-9_]+\\)>>")
+ (sqlind-match-string 1) "")))
+ ;; start of loop. this always starts a block, we only check if
+ ;; the labels match
+ (if (null sqlind-end-stmt-stack)
+ (throw 'finished (list 'in-block 'loop loop-label))
+ (destructuring-bind (pos kind label)
+ (pop sqlind-end-stmt-stack)
+ (unless (and (eq kind 'loop)
+ (sqlind-labels-match label loop-label))
+ (throw 'finshed
+ (list 'syntax-error
+ "bad closing for loop block" (point)
pos)))))))))))
+
+(defun sqlind-maybe-begin-statement ()
+ "If (point) is on a BEGIN statement, report its syntax
+Only keywords in program code are matched, not the ones inside
+expressions.
+
+See also `sqlind-beginning-of-block'"
+ (when (looking-at "begin")
+ ;; a begin statement starts a block unless it is the first begin in a
+ ;; procedure over which we need to skip it.
+ (prog1 t ; make sure we return t
+ (let* ((saved-pos (point))
+ (begin-label (save-excursion
+ (sqlind-beginning-of-statement)
+ (if (looking-at "<<\\([a-z0-9_]+\\)>>\\_>")
+ (sqlind-match-string 1) "")))
+ (previous-block (save-excursion
+ (forward-char -1)
+ (cons (sqlind-beginning-of-block) (point))))
+ (previous-block-kind (nth 0 previous-block)))
+
+ (goto-char saved-pos)
+ (when (null sqlind-end-stmt-stack)
+ (throw 'finished
+ (cond ((memq previous-block-kind '(toplevel declare-statement))
+ (list 'in-begin-block 'toplevel-block begin-label))
+ ((and (listp previous-block-kind)
+ (eq (nth 0 previous-block-kind) 'defun-start))
+ (list 'in-begin-block 'defun (nth 1 previous-block-kind)))
+ (t
+ (list 'in-begin-block nil begin-label)))))
+
+ (destructuring-bind (pos kind label) (pop sqlind-end-stmt-stack)
+ (cond
+ (kind
+ (throw 'finished
+ (list 'syntax-error "bad closing for begin block" (point) pos)))
+
+ ((not (equal begin-label ""))
+ ;; we have a begin label. In this case it must match the end
+ ;; label
+ (unless (equal begin-label label)
+ (throw 'finished
+ (list 'syntax-error "mismatched block labels" (point) pos))))
+
+ (t
+ ;; we don't have a begin label. In this case, the begin
+ ;; statement might not start a block if it is the begining of a
+ ;; procedure or declare block over which we need to skip.
+
+ (cond ((memq previous-block-kind '(toplevel declare-statement))
+ (goto-char (cdr previous-block)))
+ ((and (listp previous-block-kind)
+ (eq (nth 0 previous-block-kind) 'defun-start))
+ (unless (sqlind-labels-match
+ label (nth 1 previous-block-kind))
+ (throw 'finished
+ (list 'syntax-error
+ "bad end label for defun" (point) pos)))
+ (goto-char (cdr previous-block)))))))))))
+
+(defun sqlind-maybe-declare-statement ()
+ "If (point) is on a DECLARE statement, report its syntax.
+Only keywords in program code are matched, not the ones inside
+expressions, also we don't match DECLARE directives here.
+
+See also `sqlind-beginning-of-block'"
+ (when (looking-at "declare")
+ ;; a declare block is always toplevel, if it is not, its an error
+ (throw 'finished
+ (if (null sqlind-end-stmt-stack)
+ 'declare-statement
+ (list 'syntax-error "nested declare block" (point) (point))))))
+
+(defun sqlind-maybe-create-statement ()
+ "If (point) is on a CREATE statement, report its syntax.
+See also `sqlind-beginning-of-block'"
+ (when (looking-at "create\\_>\\(?:[ \n\r\f]+\\)\\(or\\(?:[
\n\r\f]+\\)replace\\_>\\)?")
+ (prog1 t ; make sure we return t
+ (save-excursion
+ ;; let's see what are we creating
+ (goto-char (match-end 0))
+ (sqlind-forward-syntactic-ws)
+ (let ((what (intern (downcase (buffer-substring-no-properties
+ (point)
+ (progn (forward-word) (point))))))
+ (name (downcase (buffer-substring-no-properties
+ (progn (sqlind-forward-syntactic-ws) (point))
+ (progn (skip-syntax-forward "w_") (point))))))
+ (when (and (eq what 'package) (equal name "body"))
+ (setq what 'package-body)
+ (setq name (downcase
+ (buffer-substring-no-properties
+ (progn (sqlind-forward-syntactic-ws) (point))
+ (progn (skip-syntax-forward "w_") (point))))))
+
+ (if (memq what '(procedure function package package-body))
+ (if (null sqlind-end-stmt-stack)
+ (throw 'finished
+ (list (if (memq what '(procedure function)) 'defun-start what)
+ name))
+ (destructuring-bind (pos kind label) (pop sqlind-end-stmt-stack)
+ (when (not (eq kind nil))
+ (throw 'finished
+ (list 'syntax-error
+ "bad closing for create block" (point) pos)))
+ (unless (sqlind-labels-match label name)
+ (throw 'finished
+ (list 'syntax-error
+ "label mismatch in create block" (point) pos)))))
+ ;; we are creating a non-code block thing: table, view,
+ ;; index, etc. These things only exist at toplevel.
+ (unless (null sqlind-end-stmt-stack)
+ (throw 'finised
+ (list 'syntax-error "nested create statement" (point) (point))))
+ (throw 'finished (list 'create-statement what name))))))))
+
+(defun sqlind-maybe-defun-statement ()
+ "If (point) is on a procedure definition statement, report its syntax.
+See also `sqlind-beginning-of-block'"
+ (catch 'exit
+ (when (looking-at "\\(procedure\\|function\\)\\(?:[
\n\r\f]+\\)\\([a-z0-9_]+\\)")
+ (prog1 t ; make sure we return t
+ (let ((proc-name (sqlind-match-string 2)))
+ ;; need to find out if this is a procedure/function
+ ;; declaration or a definition
+ (save-excursion
+ (goto-char (match-end 0))
+ (sqlind-forward-syntactic-ws)
+ ;; skip param list, if any.
+ (when (looking-at "(") (ignore-errors (forward-sexp 1)))
+ (sqlind-forward-syntactic-ws)
+ (when (looking-at ";")
+ ;; not a procedure after all.
+ (throw 'exit nil)))
+
+ ;; so it is a definition
+
+ ;; if the procedure starts with "create or replace", move
+ ;; point to the real start
+ (let ((real-start (point)))
+ (save-excursion
+ (forward-word -1)
+ (when (looking-at "replace")
+ (forward-word -2))
+ (when (and (looking-at "create\\([ \t\r\n\f]+or[
\t\r\n\f]+replace\\)?")
+ (not (sqlind-in-comment-or-string (point))))
+ (setq real-start (point))))
+ (goto-char real-start))
+
+ (when (null sqlind-end-stmt-stack)
+ (throw 'finished (list 'defun-start proc-name)))
+
+ (destructuring-bind (pos kind label) (pop sqlind-end-stmt-stack)
+ (unless (and (eq kind nil)
+ (sqlind-labels-match label proc-name))
+ (throw 'finished
+ (list 'syntax-error "bad end label for defun" (point)
pos)))))))))
+
+(defconst sqlind-start-block-regexp
+ (concat "\\(\\b"
+ (regexp-opt '("then" "else" "elsif" "loop"
+ "begin" "declare" "create"
+ "procedure" "function" "end") t)
+ "\\b\\)\\|)")
+ "regexp to match the start of a block")
+
+(defun sqlind-beginning-of-block (&optional end-statement-stack)
+ "Find the start of the current block and return its syntax."
+ (interactive)
+ (catch 'finished
+ (let ((sqlind-end-stmt-stack end-statement-stack))
+ (while (re-search-backward sqlind-start-block-regexp nil 'noerror)
+ (or (sqlind-in-comment-or-string (point))
+ (when (looking-at ")") (forward-char 1) (forward-sexp -1) t)
+ (sqlind-maybe-end-statement)
+ (sqlind-maybe-then-statement)
+ (sqlind-maybe-else-statement)
+ (sqlind-maybe-loop-statement)
+ (sqlind-maybe-begin-statement)
+ (sqlind-maybe-declare-statement)
+ (sqlind-maybe-create-statement)
+ (sqlind-maybe-defun-statement))))
+ 'toplevel))
+
+;;;;; Determine the syntax inside a case expression
+
+(defconst sqlind-case-clauses-regexp
+ "\\_<\\(when\\|then\\|else\\|end\\)\\_>")
+
+(defun sqlind-syntax-in-case (pos start)
+ "Return the syntax inside a CASE expression begining at START."
+ (save-excursion
+ (goto-char pos)
+ (cond ((looking-at "when\\|end")
+ ;; A WHEN, or END clause is indented relative to the start of the
case
+ ;; expression
+ (cons 'case-clause start))
+ ((looking-at "then\\|else")
+ ;; THEN and ELSE clauses are indented relative to the start of the
+ ;; when clause, which we must find
+ (while (not (and (re-search-backward "\\bwhen\\b")
+ (sqlind-same-level-statement (point) start)))
+ nil)
+ (cons 'case-clause-item (point)))
+ (t
+ ;; It is a statement continuation from the closest CASE element
+ (while (not (and (re-search-backward sqlind-case-clauses-regexp
start 'noerror)
+ (sqlind-same-level-statement (point) start)))
+ nil)
+ (cons 'case-clause-item-cont (point))))))
+
+;;;;; Determine the syntax inside a with statement
+
+(defconst sqlind-with-clauses-regexp
+ "\\_<\\(with\\|recursive\\)\\_>")
+
+(defun sqlind-syntax-in-with (pos start)
+ "Return the syntax inside a WITH statement beginning at START."
+ (save-excursion
+ (catch 'finished
+ (goto-char pos)
+ (cond
+ ((looking-at sqlind-with-clauses-regexp)
+ (throw 'finished (cons 'with-clause start)))
+ ((and (looking-at "\\_<select\\_>")
+ (sqlind-same-level-statement (point) start))
+ (throw 'finished (cons 'with-clause start))))
+ (while (re-search-backward "\\_<select\\_>" start 'noerror)
+ (when (sqlind-same-level-statement (point) start)
+ (throw 'finished (sqlind-syntax-in-select pos (point)))))
+ (goto-char pos)
+ (when (looking-at "\\_<as\\_>")
+ (throw 'finished (cons 'with-clause-cte-cont start)))
+ (sqlind-backward-syntactic-ws)
+ (when (looking-at ",")
+ (throw 'finished (cons 'with-clause-cte start)))
+ (forward-word -1)
+ (when (looking-at sqlind-with-clauses-regexp)
+ ;; We're right after the with (recursive keyword)
+ (throw 'finished (cons 'with-clause-cte start)))
+ (throw 'finished (cons 'with-clause-cte-cont start)))))
+
+;;;;; Determine the syntax inside a select statement
+
+(defconst sqlind-select-clauses-regexp
+ (concat
+ "\\_<\\("
+ "\\(union\\|intersect\\|minus\\)?[ \t\r\n\f]*select\\|"
+ "\\(bulk[ \t\r\n\f]+collect[ \t\r\n\f]+\\)?into\\|"
+ "from\\|"
+ "where\\|"
+ "order[ \t\r\n\f]+by\\|"
+ "having\\|"
+ "group[ \t\r\n\f]+by"
+ "\\)\\_>"))
+
+(defconst sqlind-select-join-regexp
+ (concat "\\b"
+ (regexp-opt '("inner" "left" "right" "natural" "cross") t)
+ "[ \t\r\n\f]*join"
+ "\\b"))
+
+(defun sqlind-syntax-in-select (pos start)
+ "Return the syntax inside a SELECT statement beginning at START"
+ (save-excursion
+ (catch 'finished
+ (goto-char pos)
+
+ ;; all select query components are indented relative to the
+ ;; start of the select statement)
+ (when (looking-at sqlind-select-clauses-regexp)
+ (throw 'finished (cons 'select-clause start)))
+
+ ;; when we are not looking at a select component, find the
+ ;; nearest one from us.
+
+ (while (re-search-backward sqlind-select-clauses-regexp start t)
+ (let* ((match-pos (match-beginning 0))
+ (clause (sqlind-match-string 0)))
+ (setq clause (replace-regexp-in-string "[ \t\r\n\f]" " " clause))
+ (when (sqlind-same-level-statement (point) start)
+ (cond
+ ((looking-at "select\\(\\s *\\_<\\(top\\s
+[0-9]+\\|distinct\\|unique\\)\\_>\\)?")
+ ;; we are in the column selection section.
+ (goto-char pos)
+ (sqlind-backward-syntactic-ws)
+ (throw 'finished
+ (cons (if (or (eq (match-end 0) (1+ (point)))
+ (looking-at ","))
+ 'select-column
+ 'select-column-continuation)
+ match-pos)))
+
+ ((looking-at "from")
+ ;; FROM is only keyword if the previous char is NOT a
+ ;; comma ','
+ (forward-char -1)
+ (sqlind-backward-syntactic-ws)
+ (unless (looking-at ",")
+ ;; yep, we are in the from section.
+ ;; if this line starts with 'on' or the previous line
+ ;; ends with 'on' we have a join condition
+ (goto-char pos)
+ (when (or (looking-at "on")
+ (progn (forward-word -1) (looking-at "on")))
+ ;; look for the join start, that will be the anchor
+ (while (re-search-backward sqlind-select-join-regexp start t)
+ (unless (sqlind-in-comment-or-string (point))
+ (throw 'finished
+ (cons 'select-join-condition (point))))))
+
+ ;; if this line starts with a ',' or the previous
+ ;; line starts with a ',', we have a new table
+ (goto-char pos)
+ (when (or (looking-at ",")
+ (progn
+ (sqlind-backward-syntactic-ws)
+ (looking-at ",")))
+ (throw 'finished (cons 'select-table match-pos)))
+
+ ;; otherwise, we continue the table definition from
+ ;; the previous line.
+ (throw 'finished (cons 'select-table-continuation match-pos))))
+
+ (t
+ (throw 'finished
+ (cons (list 'in-select-clause clause) match-pos))))))))))
+
+
+;;;;; Determine the syntax inside an insert statement
+
+(defconst sqlind-insert-clauses-regexp
+ "\\_<\\(insert\\([ \t\r\n\f]+into\\)?\\|values\\|select\\)\\_>")
+
+(defun sqlind-syntax-in-insert (pos start)
+ "Return the syntax inside an INSERT statement starting at START."
+
+ ;; The insert clause is really easy since it has the form insert into TABLE
+ ;; (COLUMN_LIST) values (VALUE_LIST) or insert into TABLE select
+ ;; SELECT_CLAUSE
+ ;;
+ ;; note that we will never be called when point is in COLUMN_LIST or
+ ;; VALUE_LIST, as that is a nested-statement-continuation which starts with
+ ;; the open paranthesis.
+ ;;
+ ;; if we are inside the SELECT_CLAUSE, we delegate the syntax to
+ ;; `sqlind-syntax-in-select'
+
+ (save-excursion
+ (catch 'finished
+ (goto-char pos)
+
+ ;; all select query components are indented relative to the
+ ;; start of the select statement)
+ (when (looking-at sqlind-insert-clauses-regexp)
+ (throw 'finished (cons 'insert-clause start)))
+
+ (while (re-search-backward sqlind-insert-clauses-regexp start t)
+ (let* ((match-pos (match-beginning 0))
+ (clause (sqlind-match-string 0)))
+ (setq clause (replace-regexp-in-string "[ \t\r\n\f]" " " clause))
+ (when (sqlind-same-level-statement (point) start)
+ (throw 'finished
+ (if (looking-at "select")
+ (sqlind-syntax-in-select pos match-pos)
+ (cons (list 'in-insert-clause clause) match-pos)))))))))
+
+
+;;;;; Determine the syntax inside a delete statement
+
+(defconst sqlind-delete-clauses-regexp
+ "\\_<\\(delete\\([ \t\r\n\f]+from\\)?\\|where\\|returning\\|\\(bulk[
\t\r\n\f]collect[ \t\r\n\f]\\)?into\\)\\_>")
+
+(defun sqlind-syntax-in-delete (pos start)
+ "Return the syntax inside a DELETE statement starting at START."
+ (save-excursion
+ (catch 'finished
+ (goto-char pos)
+
+ ;; all select query components are indented relative to the
+ ;; start of the select statement)
+ (when (looking-at sqlind-delete-clauses-regexp)
+ (throw 'finished (cons 'delete-clause start)))
+
+ (while (re-search-backward sqlind-delete-clauses-regexp start t)
+ (let* ((match-pos (match-beginning 0))
+ (clause (sqlind-match-string 0)))
+ (setq clause (replace-regexp-in-string "[ \t\r\n\f]" " " clause))
+ (when (sqlind-same-level-statement (point) start)
+ (throw 'finished
+ (cons (list 'in-delete-clause clause) match-pos))))))))
+
+
+;;;;; Determine the syntax inside an update statement
+
+(defconst sqlind-update-clauses-regexp
+ "\\_<\\(update\\|set\\|where\\)\\_>")
+
+(defun sqlind-syntax-in-update (pos start)
+ "Return the syntax inside an UPDATE statement starting at START."
+ (save-excursion
+ (catch 'finished
+ (goto-char pos)
+
+ ;; all select query components are indented relative to the start of the
+ ;; select statement)
+ (when (looking-at sqlind-update-clauses-regexp)
+ (throw 'finished (cons 'update-clause start)))
+
+ (while (re-search-backward sqlind-update-clauses-regexp start t)
+ (let* ((match-pos (match-beginning 0))
+ (clause (sqlind-match-string 0)))
+ (setq clause (replace-regexp-in-string "[ \t\r\n\f]" " " clause))
+ (when (sqlind-same-level-statement (point) start)
+ (throw 'finished
+ (cons (list 'in-update-clause clause) match-pos))))))))
+
+
+;;;;; Refine the syntax of an end statement.
+
+(defun sqlind-refine-end-syntax (end-kind end-label end-pos context)
+ (catch 'done
+
+ (when (null context) ; can happen
+ (throw 'done
+ (cons (list 'syntax-error "end statement closes nothing"
+ end-pos end-pos)
+ end-pos)))
+
+ (let* ((syntax (car (car context)))
+ (anchor (cdr (car context)))
+ (syntax-symbol (if (symbolp syntax) syntax (nth 0 syntax))))
+ (cond
+ ((memq syntax-symbol '(package package-body))
+ ;; we are closing a package declaration or body, `end-kind' must be
+ ;; empty, `end-label' can be empty or it must match the package name
+ (throw 'done
+ (cons
+ (cond (end-kind ; no end-kind is allowed for a package
+ (list 'syntax-error
+ "bad closing for package" anchor end-pos))
+ ((sqlind-labels-match end-label (nth 1 syntax))
+ (list 'block-end syntax-symbol (nth 1 syntax)))
+ (t
+ (list 'syntax-error "mismatched end label for package"
+ anchor end-pos)))
+ anchor)))
+
+ ((eq syntax-symbol 'in-begin-block)
+ ;; we are closing a begin block (either toplevel, procedure/function
+ ;; or a simple begin block. `end-kind' must be empty, `end-label'
+ ;; can be empty or it must match the pakage-name
+ (let ((block-label (nth 2 syntax)))
+ (throw 'done
+ (cons
+ (cond (end-kind ; no end-kind is allowed for a begin block
+ (list 'syntax-error
+ "bad closing for begin block" anchor end-pos))
+ ((sqlind-labels-match end-label block-label)
+ (list 'block-end (nth 1 syntax) block-label))
+ (t
+ (list 'syntax-error "mismatched end label for block"
+ anchor end-pos)))
+ anchor))))
+
+ ((eq syntax-symbol 'in-block)
+ (let ((block-kind (nth 1 syntax))
+ (block-label (nth 2 syntax)))
+ (cond
+ ((eq block-kind 'exception)
+ (goto-char anchor)
+ (forward-line -1)
+ (throw 'done
+ (sqlind-refine-end-syntax
+ end-kind end-label end-pos (sqlind-syntax-of-line))))
+
+ ((eq block-kind 'loop)
+ (throw 'done
+ (cons
+ (cond ((not (eq end-kind 'loop))
+ (list 'syntax-error "bad closing for loop block"
+ anchor end-pos))
+ ((not (sqlind-labels-match end-label block-label))
+ (list 'syntax-error "mismatched end label for loop"
+ anchor end-pos))
+ (t
+ (list 'block-end block-kind block-label)))
+ anchor)))
+
+ ((eq block-kind 'then)
+ (goto-char anchor)
+
+ (catch 'found
+ (while t
+ (let ((then-context (sqlind-syntax-of-line)))
+ (goto-char (cdar then-context))
+ (cond
+ ((looking-at "when\\_>\\|then\\_>") t)
+ ((looking-at "\\(?:<<\\([a-z0-9_]+\\)>>[
\t\r\n\f]*\\)?\\_<\\(if\\|case\\)\\_>")
+ (throw 'found t))
+ (t
+ (throw 'done
+ (cons
+ (list 'syntax-error "bad syntax start for then
keyword"
+ (point) (point))
+ anchor)))))))
+
+ (let ((start-label (or (sqlind-match-string 1) ""))
+ (start-kind (intern (sqlind-match-string 2))))
+ (throw 'done
+ (cons
+ (cond ((not (or (null end-kind) (eq end-kind start-kind)))
+ (list 'syntax-error "bad closing for if/case block"
+ (point) end-pos))
+ ((not (sqlind-labels-match end-label start-label))
+ (list 'syntax-error "mismatched labels for if/case
block"
+ (point) end-pos))
+ (t
+ (list 'block-end start-kind start-label)))
+ anchor))))
+
+ ((eq block-kind 'else)
+ ;; search the enclosing then context and refine form there. The
+ ;; `cdr' in sqlind-syntax-of-line is used to remove the
+ ;; block-start context for the else clause
+ (goto-char anchor)
+ (throw 'done
+ (sqlind-refine-end-syntax
+ end-kind end-label end-pos (cdr (sqlind-syntax-of-line)))))
+
+ ((memq block-kind '(if case))
+ (throw 'done
+ (cons
+ (cond ((not (eq end-kind block-kind))
+ (list 'syntax-error "bad closing for if/case block"
+ anchor end-pos))
+ ((not (sqlind-labels-match end-label block-label))
+ (list 'syntax-error
+ "bad closing for if/case block (label mismatch)"
+ anchor end-pos))
+ (t (list 'block-end block-kind block-label)))
+ anchor)))
+ )))
+
+ ((memq syntax-symbol '(block-start comment-start))
+ ;; there is a more generic context following one of these
+ (throw 'done
+ (sqlind-refine-end-syntax
+ end-kind end-label end-pos (cdr context))))
+
+ ((eq syntax-symbol 'defun-start)
+ (throw 'done
+ (cons
+ (if (and (null end-kind)
+ (sqlind-labels-match end-label (nth 1 syntax)))
+ (list 'block-end 'defun end-label)
+ (list 'syntax-error "mismatched end label for defun"
+ anchor end-pos))
+ anchor)))
+
+ ((eq syntax-symbol 'block-end)
+ (goto-char anchor)
+ (forward-line -1)
+ (throw 'done
+ (sqlind-refine-end-syntax
+ end-kind end-label end-pos (sqlind-syntax-of-line)))))
+
+ ;; if the above cond fell through, we have a syntax error
+ (cons (list 'syntax-error "end statement closes nothing"
+ end-pos end-pos)
+ anchor))))
+
+
+;;;;; sqlind-syntax-of-line
+
+(defun sqlind-syntax-of-line ()
+ "Return the syntax at the start of the current line.
+The function returns a list of (SYNTAX . ANCHOR) cons cells.
+SYNTAX is either a symbol or a list starting with a symbol,
+ANCHOR is a buffer position which is the reference for the
+SYNTAX. `sqlind-indentation-syntax-symbols' lists the syntax
+symbols and their meaning.
+
+Only the first element of this list is used for indentation, the
+rest are 'less specific' syntaxes, mostly left in for debugging
+purposes. "
+ (save-excursion
+ (let* ((pos (progn (back-to-indentation) (point)))
+ (context-start (progn (sqlind-beginning-of-statement) (point)))
+ (context (list (cons 'statement-continuation context-start)))
+ (have-block-context nil))
+
+ (goto-char pos)
+ (when (or (>= context-start pos)
+ (save-excursion
+ (goto-char context-start)
+ (looking-at sqlind-start-block-regexp)))
+ ;; if we are at the start of a statement, or the nearest
+ ;; statement starts after us, make the enclosing block the
+ ;; starting context
+ (setq have-block-context t)
+ (let ((block-info (sqlind-beginning-of-block)))
+
+ ;; certain kind of blocks end within a statement
+ ;; (e.g. create view). If we found one of those blocks and
+ ;; it is not within our statement, we ignore the block info.
+
+ (if (and (listp block-info)
+ (eq (nth 0 block-info) 'create-statement)
+ (not (memq (nth 1 block-info) '(function procedure)))
+ (not (eql context-start (point))))
+ (progn
+ (setq context-start (point-min))
+ (setq context (list (cons 'toplevel context-start))))
+ ;; else
+ (setq context-start (point))
+ (setq context (list (cons block-info context-start))))))
+
+ (let ((parse-info (parse-partial-sexp context-start pos)))
+ (cond ((nth 4 parse-info) ; inside a comment
+ (push (cons 'comment-continuation (nth 8 parse-info)) context))
+ ((nth 3 parse-info) ; inside a string
+ (push (cons 'string-continuation (nth 8 parse-info)) context))
+ ((> (nth 0 parse-info) 0) ; nesting
+ (let ((start (nth 1 parse-info)))
+ (goto-char (1+ start))
+ (skip-chars-forward " \t\r\n\f\v" pos)
+ (push (cons
+ (if (eq (point) pos)
+ 'nested-statement-open
+ 'nested-statement-continuation)
+ start)
+ context)))))
+
+ ;; now let's refine the syntax by adding info about the current line
+ ;; into the mix.
+
+ (let* ((most-inner-syntax (car context))
+ (syntax (car most-inner-syntax))
+ (anchor (cdr most-inner-syntax))
+ (syntax-symbol (if (symbolp syntax) syntax (nth 0 syntax))))
+
+ (goto-char pos)
+
+ (cond
+ ;; do we start a comment?
+ ((and (not (eq syntax-symbol 'comment-continuation))
+ (looking-at sqlind-comment-start-skip))
+ (push (cons 'comment-start anchor) context))
+
+ ;; Refine a statement continuation
+ ((memq syntax-symbol '(statement-continuation
nested-statement-continuation))
+
+ ;; a (nested) statement continuation which starts with loop
+ ;; or then is a block start
+ (if (and have-block-context (looking-at
"\\(loop\\|then\\|when\\)\\_>"))
+ (push (cons (list 'block-start (intern (sqlind-match-string 0)))
anchor)
+ context)
+ ;; else
+ (goto-char anchor)
+ (when (eq syntax 'nested-statement-continuation)
+ (forward-char 1)
+ (skip-chars-forward " \t\r\n\f\v")
+ (setq anchor (point)))
+
+ ;; when all we have before `pos' is a label, we have a
+ ;; labeled-statement-start
+ (if (looking-at "<<\\([a-z0-9_]+\\)>>")
+ (progn
+ (goto-char (match-end 0))
+ (forward-char 1)
+ (sqlind-forward-syntactic-ws)
+ (when (eq (point) pos)
+ (push (cons 'labeled-statement-start anchor) context)))
+
+ ;; else, maybe we have a DML statement (select, insert,
+ ;; update and delete)
+
+ ;; skip a cursor definition if it is before our point
+ (when (looking-at "cursor[ \t\r\n\f]+[a-z0-9_]+[
\t\r\n\f]+is[ \t\r\n\f]+")
+ (when (<= (match-end 0) pos)
+ (goto-char (match-end 0))))
+
+ ;; skip a forall statement if it is before our point
+ (when (looking-at "forall\\b")
+ (when (re-search-forward
"\\b\\(select\\|update\\|delete\\|insert\\)\\b" pos 'noerror)
+ (goto-char (match-beginning 0))))
+
+ ;; only check for syntax inside DML clauses if we are not
+ ;; at the start of one.
+ (when (< (point) pos)
+ (cond
+ ;; NOTE: We only catch here "CASE" clauses which start
+ ;; inside a nested paranthesis
+ ((looking-at "case")
+ (push (sqlind-syntax-in-case pos (point)) context))
+ ((looking-at "with")
+ (push (sqlind-syntax-in-with pos (point)) context))
+ ((looking-at "select")
+ (push (sqlind-syntax-in-select pos (point)) context))
+ ((looking-at "insert")
+ (push (sqlind-syntax-in-insert pos (point)) context))
+ ((looking-at "delete")
+ (push (sqlind-syntax-in-delete pos (point)) context))
+ ((looking-at "update")
+ (push (sqlind-syntax-in-update pos (point)) context))))
+
+ ;; (when (eq (car (car context))
'select-column-continuation)
+ ;; ;; case expressions can show up here, maybe refine this
+ ;; ;; syntax
+ ;; t
+ ;; )
+
+ )))
+
+ ;; create block start syntax if needed
+
+ ((and (eq syntax-symbol 'in-block)
+ (memq (nth 1 syntax) '(if elsif then case))
+ (looking-at "\\(then\\|\\(els\\(e\\|if\\)\\)\\)\\_>"))
+ (let ((what (intern (sqlind-match-string 0))))
+ ;; the only invalid combination is a then statement in
+ ;; an (in-block "then") context
+ (unless (and (eq what 'then) (equal (nth 1 syntax) 'then))
+ (push (cons (list 'block-start what) anchor) context))))
+
+ ;; note that begin is not a block-start in a 'in-begin-block
+ ;; context
+ ((and (memq syntax-symbol '(defun-start declare-statement toplevel))
+ (looking-at "begin\\_>"))
+ (push (cons (list 'block-start 'begin) anchor) context))
+
+ ((and (memq syntax-symbol '(defun-start package package-body))
+ (looking-at "\\(is\\|as\\)\\_>"))
+ (push (cons (list 'block-start 'is-or-as) anchor) context))
+
+ ((and (memq syntax-symbol '(in-begin-block in-block))
+ (looking-at "exception\\_>"))
+ (push (cons (list 'block-start 'exception) anchor) context))
+
+ ((and (eq syntax-symbol 'in-block)
+ (memq (nth 1 syntax) '(then case))
+ (looking-at "when\\_>"))
+ (push (cons (list 'block-start 'when) anchor) context))
+
+ ;; indenting the select clause inside a view
+ ((and (eq syntax-symbol 'create-statement)
+ (eq (nth 1 syntax) 'view))
+ (goto-char anchor)
+ (catch 'done
+ (while (re-search-forward "\\bselect\\b" pos 'noerror)
+ (goto-char (match-beginning 0))
+ (when (sqlind-same-level-statement (point) anchor)
+ (push (sqlind-syntax-in-select pos (point)) context)
+ (throw 'done nil))
+ (goto-char (match-end 0)))))
+
+ ;; create a block-end syntax if needed
+
+ ((and (not (eq syntax-symbol 'comment-continuation))
+ (looking-at "end[
\t\r\n\f]*\\(\\_<\\(?:if\\|loop\\|case\\)\\_>\\)?[
\t\r\n\f]*\\(\\_<\\(?:[a-z0-9_]+\\)\\_>\\)?"))
+ ;; so we see the syntax which closes the current block. We still
+ ;; need to check if the current end is a valid closing block
+ (let ((kind (or (sqlind-match-string 1) ""))
+ (label (or (sqlind-match-string 2) "")))
+ (push (sqlind-refine-end-syntax
+ (if (equal kind "") nil (intern kind))
+ label (point) context)
+ context)))))
+ context)))
+
+(defun sqlind-show-syntax-of-line ()
+ "Print the syntax of the current line."
+ (interactive)
+ (prin1 (sqlind-syntax-of-line)))
+
+
+;;;; Indentation of SQL code
+
+(defvar sqlind-basic-offset 2
+ "The basic indentaion amount for SQL code.
+Indentation is usually done in multiples of this amount, but
+special indentation functions can do other types of indentation
+such as aligning. See also `sqlind-indentation-offsets-alist'.")
+
+(defvar sqlind-indentation-syntax-symbols '()
+ "This variable exists just for its documentation.
+
+The the SQL parsing code returns a syntax definition (either a
+symbol or a list) and an anchor point, which is a buffer
+position. They syntax symbols can be used to define how to
+indent each line, see `sqlind-indentation-offsets-alist'
+
+The following syntax symbols are defined for SQL code:
+
+- (syntax-error MESSAGE START END) -- this is returned when the
+ parse failed. MESSAGE is an informative message, START and END
+ are buffer locations denoting the problematic region. ANCHOR
+ is undefined for this syntax info
+
+- in-comment -- line is inside a multi line comment, ANCHOR is
+ the start of the comment.
+
+- comment-start -- line starts with a comment. ANCHOR is the
+ start of the enclosing block.
+
+- in-string -- line is inside a string, ANCHOR denotes the start
+ of the string.
+
+- toplevel -- line is at toplevel (not inside any programming
+ construct). ANCHOR is usually (point-min).
+
+- (in-block BLOCK-KIND LABEL) -- line is inside a block
+ construct. BLOCK-KIND (a symbol) is the actual block type and
+ can be one of \"if\", \"case\", \"exception\", \"loop\" etc.
+ If the block is labeled, LABEL contains the label. ANCHOR is
+ the start of the block.
+
+- (in-begin-block KIND LABEL) -- line is inside a block started
+ by a begin statement. KIND (a symbol) is \"toplevel-block\"
+ for a begin at toplevel, \"defun\" for a begin that starts the
+ body of a procedure or function, nil for a begin that is none
+ of the previous. For a \"defun\", LABEL is the name of the
+ procedure or function, for the other block types LABEL contains
+ the block label, or the empty string if the block has no label.
+ ANCHOR is the start of the block.
+
+- (block-start KIND) -- line begins with a statement that starts
+ a block. KIND (a symbol) can be one of \"then\", \"else\" or
+ \"loop\". ANCHOR is the reference point for the block
+ start (the coresponding if, case, etc).
+
+- (block-end KIND LABEL) -- the line contains an end statement.
+ KIND (a symbol) is the type of block we are closing, LABEL (a
+ string) is the block label (or procedure name for an end
+ defun).
+
+- declare-statement -- line is after a declare keyword, but
+ before the begin. ANCHOR is the start of the declare
+ statement.
+
+- (package NAME) -- line is inside a package definition. NAME is
+ the name of the package, ANCHOR is the start of the package.
+
+- (package-body NAME) -- line is inside a package body. NAME is
+ the name of the package, ANCHOR is the start of the package
+ body.
+
+- (create-statement WHAT NAME) -- line is inside a CREATE
+ statement (other than create procedure or function). WHAT is
+ the thing being created, NAME is its name. ANCHOR is the start
+ of the create statement.
+
+- (defun-start NAME) -- line is inside a procedure of function
+ definition but before the begin block that starts the body.
+ NAME is the name of the procedure/function, ANCHOR is the start
+ of the procedure/function definition.
+
+The following SYNTAX-es are for SQL statements. For all of
+them ANCHOR points to the start of a statement itself.
+
+- labeled-statement-start -- line is just after a label.
+
+- statement-continuation -- line is inside a statement which
+ starts on a previous line.
+
+- nested-statement-open -- line is just inside an opening
+ bracket, but the actual bracket is on a previous line.
+
+- nested-statement-continuation -- line is inside an opening
+ bracket, but not the first element after the bracket.
+
+The following SYNTAX-es are for statements which are SQL
+code (DML statements). They are pecialisations on the previous
+statement syntaxes and for all of them a previous generic
+statement syntax is present earlier in the SYNTAX list. Unless
+otherwise specified, ANCHOR points to the start of the
+clause (select, from, where, etc) in which the current point is.
+
+- with-clause -- line is inside a WITH clause, but before the
+ main SELECT clause.
+
+- with-clause-cte -- line is inside a with clause before a
+ CTE (common table expression) declaration
+
+- with-clause-cte-cont -- line is inside a with clause before a
+ CTE definition
+
+- case-clause -- line is on a CASE expression (WHEN or END
+ clauses). ANCHOR is the start of the CASE expression.
+
+- case-clause-item -- line is on a CASE expression (THEN and ELSE
+ clauses). ANCHOR is the position of the case clause.
+
+- case-clause-item-cont -- line is on a CASE expression but not
+ on one of the CASE sub-keywords. ANCHOR points to the case
+ keyword that this line is a continuation of.
+
+- select-clause -- line is inside a select statement, right
+ before one of its clauses (from, where, order by, etc).
+
+- select-column -- line is inside the select column section,
+ after a full column was defined (and a new column definition is
+ about to start).
+
+- select-column-continuation -- line is inside the select column
+ section, but in the middle of a column definition. The defined
+ column starts on a previous like. Note that ANCHOR still
+ points to the start of the select statement itself.
+
+- select-join-condition -- line is right before or just after the ON clause
+ for an INNER, LEFT or RIGHT join. ANCHOR points to the join statement
+ for which the ON is defined.
+
+- select-table -- line is inside the from clause, just after a
+ table was defined and a new one is about to start.
+
+- select-table-continuation -- line is inside the from clause,
+ inside a table definition which starts on a previous line. Note
+ that ANCHOR still points to the start of the select statement
+ itself.
+
+- (in-select-clause CLAUSE) -- line is inside the select CLAUSE,
+ which can be \"where\", \"order by\", \"group by\" or
+ \"having\". Note that CLAUSE can never be \"select\" and
+ \"from\", because we have special syntaxes inside those
+ clauses.
+
+- insert-clause -- line is inside an insert statement, right
+ before one of its clauses (values, select).
+
+- (in-insert-clause CLAUSE) -- line is inside the insert CLAUSE,
+ which can be \"insert into\" or \"values\".
+
+- delete-clause -- line is inside a delete statement right before
+ one of its clauses.
+
+- (in-delete-clause CLAUSE) -- line is inside a delete CLAUSE,
+ which can be \"delete from\" or \"where\".
+
+- update-clause line is inside an update statement right before
+ one of its clauses.
+
+- (in-update-clause CLAUSE) -- line is inside an update CLAUSE,
+ which can be \"update\", \"set\" or \"where\".")
+
+(defvar sqlind-indentation-offsets-alist
+ '((syntax-error sqlind-report-sytax-error)
+ (in-string sqlind-report-runaway-string)
+
+ (comment-continuation sqlind-indent-comment-continuation)
+ (comment-start sqlind-indent-comment-start)
+
+ (toplevel 0)
+ (in-block +)
+ (in-begin-block +)
+ (block-start 0)
+ (block-end 0)
+ (declare-statement +)
+ (package ++)
+ (package-body 0)
+ (create-statement +)
+ (defun-start +)
+ (labeled-statement-start 0)
+ (statement-continuation +)
+ (nested-statement-open sqlind-use-anchor-indentation +)
+ (nested-statement-continuation sqlind-use-previous-line-indentation)
+
+ (with-clause sqlind-use-anchor-indentation)
+ (with-clause-cte +)
+ (with-clause-cte-cont ++)
+ (case-clause 0)
+ (case-clause-item sqlind-use-anchor-indentation +)
+ (case-clause-item-cont sqlind-right-justify-clause)
+ (select-clause sqlind-right-justify-clause)
+ (select-column sqlind-indent-select-column)
+ (select-column-continuation sqlind-indent-select-column +)
+ (select-join-condition ++)
+ (select-table sqlind-indent-select-table)
+ (select-table-continuation sqlind-indent-select-table +)
+ (in-select-clause sqlind-lineup-to-clause-end)
+ (insert-clause sqlind-right-justify-clause)
+ (in-insert-clause sqlind-lineup-to-clause-end)
+ (delete-clause sqlind-right-justify-clause)
+ (in-delete-clause sqlind-lineup-to-clause-end)
+ (update-clause sqlind-right-justify-clause)
+ (in-update-clause sqlind-lineup-to-clause-end))
+ "Define the indentation amount for each syntactic symbol.
+
+The value of this variable is an ALIST with the format:
+
+ ((SYNTACTIC-SYMBOL . INDENTATION-OFFSETS) ... )
+
+`sqlind-indentation-syntax-symbols' documents the list of possible
+SYNTACTIC-SYMBOL values.
+
+INDENTATION-OFFSETS is a list of:
+
+ a NUMBER -- the indentation offset will be set to that number
+
+ '+ -- the current indentation offset is incremented by
+ `sqlind-basic-offset'
+
+ '++ -- the current indentation offset is indentation by 2 *
+ `sqlind-basic-offset'
+
+ '- -- the current indentation offset is decremented by
+ `sqlind-basic-offset'
+
+ '-- -- the current indentation offset is decremented by 2 *
+ `sqlind-basic-offset'
+
+ a FUNCTION -- the syntax and current indentation offset is
+ passed to the function and its result is used as the new
+ indentation offset.
+
+See `sqlind-calculate-indentation' for how the indentation offset
+is calculated.")
+
+(defun sqlind-calculate-indentation (syntax &optional base-indentation)
+ "Return the indentation that should be applied to the current line.
+SYNTAX is the syntaxtic information as returned by
+`sqlind-syntax-of-line', BASE-INDENTATION is an indentation offset
+to start with. When BASE-INDENTATION is nil, it is initialised
+to the column of the anchor.
+
+The indentation is done as follows: first, the indentation
+offsets for the current syntactic symbol is looked up in
+`sqlind-indentation-offsets-alist'. Than, for each indentation
+offset, BASE-INDENTATION is adjusted according to that
+indentation offset. The final value of BASE-INDENTATION is than
+returned."
+ (if (null syntax)
+ base-indentation
+ ;; else
+
+ ;; when the user did not specify a base-indentation, we use the
+ ;; column of the anchor as a starting point
+ (when (null base-indentation)
+ (setq base-indentation (save-excursion
+ (goto-char (cdar syntax))
+ (current-column))))
+
+ (let* ((this-syntax (caar syntax))
+ (syntax-symbol (if (symbolp this-syntax)
+ this-syntax
+ (nth 0 this-syntax)))
+ (indent-info (cdr (assoc syntax-symbol
+ sqlind-indentation-offsets-alist)))
+ (new-indentation base-indentation))
+
+ ;; the funcall below can create a nil indentation symbol to
+ ;; abort the indentation process
+ (while (and new-indentation indent-info)
+ (let ((i (car indent-info)))
+ (setq new-indentation
+ (cond
+ ((eq i '+) (+ new-indentation sqlind-basic-offset))
+ ((eq i '++) (+ new-indentation (* 2 sqlind-basic-offset)))
+ ((eq i '-) (- new-indentation sqlind-basic-offset))
+ ((eq i '--) (- new-indentation (* 2 sqlind-basic-offset)))
+ ((integerp i) (+ new-indentation i))
+ ((functionp i) (funcall i syntax new-indentation))
+ ;; ignore unknown symbols by default
+ (t new-indentation))))
+ (setq indent-info (cdr indent-info)))
+ new-indentation)))
+
+(defun sqlind-report-sytax-error (syntax base-indentation)
+ (destructuring-bind (sym msg start end) (caar syntax)
+ (message "%s (%d %d)" msg start end))
+ nil)
+
+(defun sqlind-report-runaway-string (syntax base-indentation)
+ (message "runaway string constant")
+ nil)
+
+(defun sqlind-use-anchor-indentation (syntax base-indentation)
+ "Return the indentation of the line containing ANCHOR.
+By default, the column of the anchor position is uses as a base
+indentation. You can use this function to switch to using the
+indentation of the anchor as the base indentation."
+ (let ((anchor (cdar syntax)))
+ (save-excursion
+ (goto-char anchor)
+ (current-indentation))))
+
+(defun sqlind-use-previous-line-indentation (syntax base-indentation)
+ "Return the indentation of the previous line.
+If the start of the previous line is before the ANCHOR, use the
+column of the ANCHOR + 1."
+ (let ((anchor (cdar syntax)))
+ (save-excursion
+ (forward-line -1)
+ (back-to-indentation)
+ (if (< (point) anchor)
+ (progn
+ (goto-char anchor)
+ (1+ (current-column)))
+ (current-column)))))
+
+(defun sqlind-indent-comment-continuation (syntax base-indentation)
+ "Return the indentation proper for a line inside a comment.
+If the current line matches `sqlind-comment-prefix' or
+`sqlind-comment-end', we indent to one plus the column of the
+comment start, which will make comments line up nicely, like
+this:
+
+ /* Some comment line
+ * another comment line
+ */
+
+When the current line does not match `sqlind-comment-prefix', we
+indent it so it lines up with the text of the start of the
+comment, like this:
+
+ /* Some comment line
+ Some other comment line
+ */
+"
+ (let ((anchor (cdar syntax)))
+ (save-excursion
+ (back-to-indentation)
+ (if (or (looking-at sqlind-comment-prefix)
+ (looking-at sqlind-comment-end))
+ (progn
+ (goto-char anchor)
+ (1+ (current-column)))
+ ;; else
+ (goto-char anchor)
+ (when (looking-at sqlind-comment-start-skip)
+ (goto-char (match-end 0)))
+ (current-column)))))
+
+(defun sqlind-indent-comment-start (syntax base-indentation)
+ "Return the indentation for a comment start.
+If we start a line comment (--) and the previous line also has a
+line comment, we line up the two comments. Otherwise we indent
+in the previous context. "
+ (save-excursion
+ (back-to-indentation)
+ (if (and (looking-at "\\s *--")
+ (progn
+ (forward-line -1)
+ (re-search-forward "--" (c-point 'eol) t)))
+ (progn
+ (goto-char (match-beginning 0))
+ (current-column))
+ (sqlind-calculate-indentation (cdr syntax) base-indentation))))
+
+(defun sqlind-indent-select-column (syntax base-indentation)
+ "Return the indentation for a column of a SELECT clause.
+We try to align to the previous column start, but if we are the
+first column after the SELECT clause we simply add
+`sqlind-basic-offset'."
+ (let ((anchor (cdar syntax)))
+ (save-excursion
+ (goto-char anchor)
+ (when (looking-at "select\\s *\\(top\\s +[0-9]+\\|distinct\\|unique\\)?")
+ (goto-char (match-end 0)))
+ (skip-syntax-forward " ")
+ (if (or (looking-at sqlind-comment-start-skip)
+ (looking-at "$"))
+ (+ base-indentation sqlind-basic-offset)
+ (current-column)))))
+
+(defun sqlind-indent-select-table (syntax base-indentation)
+ "Return the indentation for a table in the FROM section.
+We try to align to the first table, but if we are the first
+table, we simply add `sqlind-basic-offset'."
+ (let ((anchor (cdar syntax)))
+ (save-excursion
+ (goto-char anchor)
+ (when (looking-at "from")
+ (goto-char (match-end 0)))
+ (skip-syntax-forward " ")
+ (if (or (looking-at sqlind-comment-start-skip)
+ (looking-at "$"))
+ (+ base-indentation sqlind-basic-offset)
+ (current-column)))))
+
+(defun sqlind-lineup-to-clause-end (syntax base-indentation)
+ "Line up the current line with the end of a query clause.
+
+This assumes SYNTAX is one of in-select-clause, in-update-clause,
+in-insert-clause or in-delete-clause. It will return an
+indentation so that:
+
+If the clause is on a line by itself, the current line is
+indented by `sqlind-basic-offset', otherwise the current line is
+indented so that it starts in next column from where the clause
+keyword ends.
+
+An exception is made for a 'where' clause: if the current line
+starts with an 'and' or an 'or' the line is indented so that the
+and/or is right justified with the 'where' clause."
+ (let ((origin (point)))
+ (destructuring-bind ((sym clause) . anchor) (car syntax)
+ (save-excursion
+ (goto-char anchor)
+ (forward-char (1+ (length clause)))
+ (skip-syntax-forward " ")
+ (if (or (looking-at sqlind-comment-start-skip)
+ (looking-at "$"))
+ ;; if the clause is on a line by itself, indent this line
+ ;; with a sqlind-basic-offset
+ (+ base-indentation sqlind-basic-offset)
+ ;; otherwise, align to the end of the clause, with a few
+ ;; exceptions
+ (let ((indentation (current-column)))
+ (goto-char origin)
+ (back-to-indentation)
+ ;; when the line starts with an 'and' or an 'or', line
+ ;; it up so that the logic operator sits right under the
+ ;; where clause
+ (when (and (equal clause "where")
+ (looking-at "and\\|or"))
+ (decf indentation (1+ (- (match-end 0) (match-beginning 0)))))
+ indentation))))))
+
+(defun sqlind-right-justify-clause (syntax base-indentation)
+ "Return an indentation which right-aligns the first word at
+ANCHOR with the first word in the curent line.
+
+This is intended to be used as an indentation function for
+select-clause, update-clause, insert-clause and update-clause
+syntaxes"
+ (save-excursion
+ (let ((clause-length 0)
+ (statement-keyword-length 0)
+ offset)
+ (back-to-indentation)
+ (when (looking-at "\\sw+\\b")
+ (setq clause-length (- (match-end 0) (match-beginning 0))))
+ (goto-char (cdar syntax)) ; move to ANCHOR
+ (when (looking-at "\\sw+\\b")
+ (setq statement-keyword-length (- (match-end 0) (match-beginning 0))))
+ (setq offset (- statement-keyword-length clause-length))
+ (if (> offset 0)
+ (+ base-indentation offset)
+ base-indentation))))
+
+
+
+(defun sqlind-indent-line ()
+ "Indent the current line according to SQL conventions.
+`sqlind-basic-offset' defined the number of spaces in the basic
+indentation unit and `sqlind-indentation-offsets-alist' is used to
+determine how to indent each type of syntactic element."
+ (let* ((syntax (sqlind-syntax-of-line))
+ (base-column (current-column))
+ (indent-column (sqlind-calculate-indentation syntax)))
+ (when indent-column
+ (back-to-indentation)
+ (let ((offset (- base-column (current-column))))
+ ;; avoid modifying the buffer when the indentation does not
+ ;; have to change
+ (unless (eq (current-column) indent-column)
+ (delete-horizontal-space)
+ (indent-to indent-column))
+ (when (> offset 0)
+ (forward-char offset))))))
+
+;;;; sqlind-setup
+
+;;;###autoload
+(defun sqlind-setup ()
+ (set-syntax-table sqlind-syntax-table)
+ (set (make-local-variable 'indent-line-function) 'sqlind-indent-line)
+ (define-key sql-mode-map [remap beginning-of-defun]
'sqlind-beginning-of-statement))
+
+(provide 'sqlind)
+
+;;; Local Variables:
+;;; mode: emacs-lisp
+;;; mode: outline-minor
+;;; outline-regexp: ";;;;+"
+;;; End: