branch: externals/auctex commit 35ec84ba6faf078b114d5df62513d81900cf3b6c Author: Arash Esbati <ar...@gnu.org> Commit: Arash Esbati <ar...@gnu.org>
Add capf for LaTeX marco/environment arguments * doc/changes.texi: Announce the new feature. * doc/auctex.texi (Completion): Document the main new function. * latex.el: Add functions for completion-at-point inside marco/environment arguments in LaTeX buffers. (LaTeX-common-initialization): Append the entry point `LaTeX--arguments-completion-at-point' to `completion-at-point-functions'. --- doc/auctex.texi | 13 +- doc/changes.texi | 6 + latex.el | 470 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 488 insertions(+), 1 deletion(-) diff --git a/doc/auctex.texi b/doc/auctex.texi index d323e6f272..5f8c725714 100644 --- a/doc/auctex.texi +++ b/doc/auctex.texi @@ -1326,7 +1326,7 @@ define and register custom @code{completion-at-point} functions and when the user invokes @code{completion-at-point} (usually bound to @kbd{M-@key{TAB}}), all such registered functions are consulted for checking for possible completions. Modern completion UIs like -@i{company-mode} support this completion-at-point facility. +@i{company-mode} or @i{corfu} support this completion-at-point facility. @defun TeX--completion-at-point @AUCTeX{}'s completion-at-point function which is automatically added to @@ -1339,6 +1339,17 @@ It offers the same completion candidates as would like @i{company-mode}. @end defun +@defun LaTeX--arguments-completion-at-point +@AUCTeX{}'s completion-at-point function inside arguments which is +automatically added to @code{completion-at-point-functions} in @LaTeX{} +buffers. + +It offers the completion candidates stored in the variables +@code{TeX-symbol-list} and @code{LaTeX-environment-list} for single +candidate, multiple candidates separated by commas, or key-value +candidates separated by commas and/or equal signs. +@end defun + A more direct way to insert a macro is with @code{TeX-insert-macro}, bound to @kbd{C-c C-m} which is equivalent to @kbd{C-c @key{RET}}. It has the advantage over completion that it knows about the argument of diff --git a/doc/changes.texi b/doc/changes.texi index debfec688b..67e9e458d4 100644 --- a/doc/changes.texi +++ b/doc/changes.texi @@ -11,6 +11,12 @@ @heading News since last release @itemize @bullet +@item +@AUCTeX{} supports completion-at-point of macro and environment arguments +in @LaTeX{} buffers. The responsible function recognizes the argument +position and extracts the corresponding candidates from the variables +@code{TeX-symbol-list} and @code{LaTeX-environment-list}. + @item @AUCTeX{} underlines the argument of macros which produce underlined text in the final product with @code{font-latex-underline-face}. The diff --git a/latex.el b/latex.el index 901ddfd7d0..0c83a3a30b 100644 --- a/latex.el +++ b/latex.el @@ -7184,6 +7184,471 @@ page without enough text on it. ") Used as buffer local value of `TeX-error-description-list-local'. See its doc string for detail.") + +;;; LaTeX Capf for macro/environment arguments: + +;; tex.el defines the function `TeX--completion-at-point' which +;; provides completion at point for (La)TeX macros. Here we define +;; `LaTeX--arguments-completion-at-point' which is the entry point for +;; completion at point when inside a macro or environment argument. +;; The general idea is: +;; +;; - Find out in which argument of macro/env the point is; this is +;; done by the function `LaTeX-what-macro'. +;; +;; - Match the result against the information available in +;; `TeX-symbol-list' or `LaTeX-environment-list' by the function +;; `LaTeX-completion-parse-args'. +;; +;; - If there is a match, pass it to `LaTeX-completion-parse-arg' +;; (note the missing `s') which parses the match and runs the +;; corresponding function to calculate the candidates. These are the +;; functions `LaTeX-completion-candidates-key-val', +;; `LaTeX-completion-candidates-completing-read-multiple', and +;; `LaTeX-completion-candidates-completing-read'. +;; +;; Two mapping variables `LaTeX-completion-function-map-alist-keyval' +;; and `LaTeX-completion-function-map-alist-cr' are provided in order +;; to allow a redirection of the entry in `TeX-symbol-list' or +;; `LaTeX-environment-list' to another function. + +(defvar LaTeX-completion-macro-delimiters + '((?\[ . ?\]) + (?\{ . ?\}) + (?\( . ?\)) + (?\< . ?\>)) + "List of characters delimiting mandatory and optional arguments. +Each element in the list is cons with opening char as car and the +closing char as cdr.") + +(defun LaTeX-completion-macro-delimiters (&optional which) + "Return elements of the variable `LaTeX-completion-macro-delimiters'. +If the optional WHICH is the symbol `open', return the car's of +each element in the variable `LaTeX-completion-macro-delimiters'. +If it is the symbol `close', return the cdr's. If omitted or +nil, return all elements." + (cond ((eq which 'open) + (mapcar #'car LaTeX-completion-macro-delimiters)) + ((eq which 'close) + (mapcar #'cdr LaTeX-completion-macro-delimiters)) + (t + (append + (mapcar #'car LaTeX-completion-macro-delimiters) + (mapcar #'cdr LaTeX-completion-macro-delimiters))))) + +(defun LaTeX-move-to-previous-arg (&optional bound) + "Move backward to the closing parenthesis of the previous argument. +Closing parenthesis is in this context all characters which can +be used to delimit an argument. Currently, these are the +following characters: + + } ] ) > + +This happens under the assumption that we are in front of a macro +argument. This function understands the splitting of macros over +several lines in TeX." + (cond + ;; Just to be quick: + ((memql (preceding-char) (LaTeX-completion-macro-delimiters 'close))) + ;; Do a search: + ((re-search-backward + "[]})>][ \t]*[\n\r]?\\([ \t]*%[^\n\r]*[\n\r]\\)*[ \t]*\\=" bound t) + (goto-char (1+ (match-beginning 0))) + t) + (t nil))) + +(defun LaTeX-what-macro (&optional bound) + "Find out if point is within the arguments of any TeX-macro. +The return value is + + (\"name\" mac-or-env total-num type opt-num opt-distance) + +\"name\" is the name of the macro (without backslash) or + environment as a string. +mac-or-env is one of the symbols `mac' or `env'. +total-num is the total number of the argument before the point started. +type is one of the symbols `mandatory' or `optional'. +opt-num is the number of optional arguments before the point started. +opt-distance the number of optional arguments after the last mandatory. + +If the optional BOUND is an integer, limit backward searches to +this point. If nil, limit to the previous 15 lines." + (let ((bound (or bound (line-beginning-position -15))) + (env-or-mac 'mac) + cmd cnt cnt-opt type result ;; env-or-mac-start + (cnt-distance 0)) + (save-excursion + (save-restriction + (narrow-to-region (max (point-min) bound) (point-max)) + ;; Move back out of the current parenthesis + (with-syntax-table (apply #'TeX-search-syntax-table + (LaTeX-completion-macro-delimiters)) + (condition-case nil + (let ((forward-sexp-function nil)) + (up-list -1)) + (error nil)) + ;; Set the initial value of argument counter + (setq cnt 1) + ;; Note that we count also the right opt. or man. arg: + (setq cnt-opt (if (= (following-char) ?\{) 0 1)) + ;; Record if we're inside a mand. or opt. argument + (setq type (if (= (following-char) ?\{) 'mandatory 'optional)) + ;; Move back over any touching sexps + (while (and (LaTeX-move-to-previous-arg bound) + (condition-case nil + (let ((forward-sexp-function nil)) + (backward-sexp) t) + (error nil))) + (unless (= (following-char) ?\{) + (cl-incf cnt-opt)) + (cl-incf cnt))) + ;; (setq env-or-mac-start (point)) + (when (and (memql (following-char) ;; '(?\[ ?\{ ?\( ?<) + (LaTeX-completion-macro-delimiters 'open)) + (re-search-backward "\\\\[*a-zA-Z]+\\=" nil t)) + (setq cmd (TeX-match-buffer 0)) + (when (looking-at "\\\\begin{\\([^}]+\\)}") + (setq cmd (TeX-match-buffer 1)) + (setq env-or-mac 'env) + (cl-decf cnt)) + (when (and cmd (not (string= cmd ""))) + (setq result (list (if (eq env-or-mac 'mac) + ;; Strip leading backslash from + ;; the macro + (substring cmd 1) + cmd) + env-or-mac cnt type cnt-opt)))))) + ;; If we were inside an optional argument after a mandatory one, + ;; we have to find out the number of optional arguments before + ;; the mandatory one. + (when (and (eq (nth 3 result) 'optional) + (/= 0 (- (nth 2 result) (nth 4 result)))) + (save-excursion + (save-restriction + (narrow-to-region (max (point-min) bound) (point-max)) + (with-syntax-table (apply #'TeX-search-syntax-table + (LaTeX-completion-macro-delimiters)) + (let ((forward-sexp-function nil)) + (up-list -1)) + (unless (= (following-char) ?\{) + (cl-incf cnt-distance)) + (while (and (LaTeX-move-to-previous-arg bound) + (condition-case nil + (let ((forward-sexp-function nil)) + (backward-sexp) + (/= (following-char) ?\{)) + (error nil))) + (cl-incf cnt-distance)))))) + ;; Check if we really have a result before adding something new: + (when result + (append result (list cnt-distance))))) + +(defun LaTeX-completion-candidates-key-val (key-vals) + "Return completion candidates from KEY-VALS based on buffer position. +KEY-VALS is an alist of key-values pairs." + (let ((end (point)) + (func (lambda (kv &optional k) + (if k + (cadr (assoc k kv)) + kv))) + beg key) + (save-excursion + (re-search-backward "[[{(<,=]" (line-beginning-position 0) t)) + (if (string= (match-string 0) "=") + ;; We have to look for a value: + (save-excursion + ;; Matching the value is easy, just grab everything before the + ;; '=' and ... + (re-search-backward "=\\([^=]*\\)" (line-beginning-position) t) + ;; ... then move forward over any tabs and spaces: + (save-excursion + (forward-char) + (skip-chars-forward " \t" end) + (setq beg (point))) + ;; Matching the key is less fun: `re-search-backward' + ;; doesn't travel enough, so we have to use + ;; `skip-chars-backward' and limit the search to the + ;; beginning of the previous line: + (skip-chars-backward "^,[{" (line-beginning-position 0)) + ;; Make sure we're not looking at a comment: + (when (looking-at-p (concat "[ \t]*" TeX-comment-start-regexp)) + (forward-line)) + ;; Now pick up the key, if available: + (setq key (string-trim + (buffer-substring-no-properties (point) + (match-beginning 0)) + "[ \t\n\r%]+" "[ \t\n\r%]+")) + ;; This caters also for the case where nothing is typed yet: + (list beg end (completion-table-dynamic + (lambda (_) + (funcall func key-vals key))))) + ;; We have to look for a key: + (save-excursion + ;; Find the beginning + (skip-chars-backward "^,[{" (line-beginning-position 0)) + ;; Make sure we're not looking at a comment: + (when (looking-at-p (concat "[ \t]*" TeX-comment-start-regexp)) + (forward-line)) + ;; Now go until the first char or number which would be the + ;; start of the key: + (skip-chars-forward "^a-zA-Z0-9" end) + (setq beg (point)) + ;; This caters also for the case where nothing is typed yet: + (list beg end (completion-table-dynamic + (lambda (_) + (funcall func key-vals)))))))) + +(defun LaTeX-completion-candidates-completing-read-multiple (collection) + "Return completion candidates from COLLECTION based on buffer position. +COLLECTION is an list of strings." + (let ((end (point)) + beg list-beg) + (save-excursion + (with-syntax-table (apply #'TeX-search-syntax-table + (LaTeX-completion-macro-delimiters)) + (up-list -1)) + (setq list-beg (1+ (point)))) + (save-excursion + (unless (search-backward "," list-beg t) + (goto-char list-beg)) + (skip-chars-forward "^a-zA-Z0-9" end) + (setq beg (point))) + (list beg end (completion-table-dynamic + (lambda (_) + collection))))) + +(defun LaTeX-completion-candidates-completing-read (collection) + "Return completion candidates from COLLECTION based on buffer position. +COLLECTION is an list of strings." + (let ((end (point)) + beg) + (save-excursion + (with-syntax-table (apply #'TeX-search-syntax-table + (LaTeX-completion-macro-delimiters)) + (up-list -1)) + (forward-char) + (skip-chars-forward "^a-zA-Z0-9" end) + (setq beg (point))) + (list beg end (completion-table-dynamic + (lambda (_) + collection))))) + +(defun LaTeX-completion-parse-args (entry) + "Return the match of buffer position ENTRY with AUCTeX macro definitions. +ENTRY is generated by the function `LaTeX-what-macro'. This +function matches the current buffer position (i.e., which macro +argument) with the corresponding definition in `TeX-symbol-list' +or `LaTeX-environment-list' and returns it." + (let* ((name (nth 0 entry)) + (mac-or-env (nth 1 entry)) + (total-num (nth 2 entry)) + (type (nth 3 entry)) + (opt-num (nth 4 entry)) + (opt-dis (nth 5 entry)) + (mand-num (- total-num opt-num)) + (cnt 0) + (again t) + arg-list + arg + result) + (setq arg-list (cdr (assoc name (if (eq mac-or-env 'mac) + (TeX-symbol-list) + (LaTeX-environment-list))))) + + ;; Check if there is a `LaTeX-env-args' in the `arg-list' and + ;; remove it: + (when (and (eq mac-or-env 'env) + (eq (car arg-list) #'LaTeX-env-args)) + (pop arg-list)) + + ;; Check for `TeX-arg-conditional' here and change `arg-list' + ;; accordingly + (when (assq 'TeX-arg-conditional arg-list) + (while (and arg-list + (setq arg (car arg-list))) + (if (and (listp arg) (eq (car arg) 'TeX-arg-conditional)) + (setq result (append (reverse (if (eval (nth 1 arg) t) + (nth 2 arg) + (nth 3 arg))) + result)) + (push arg result)) + (pop arg-list)) + (setq arg-list (nreverse result))) + + ;; Now parse the `arg-list': + (cond ((and (eq type 'optional) + (= opt-dis 0)) + ;; Optional arg without mandatory one before: This case is + ;; straight and we just pick the correct one out of the + ;; list: + (setq result (nth (1- total-num) arg-list))) + + ;; Mandatory arg: Loop over the arg-list and drop all + ;; vectors at the list beginning: + ((eq type 'mandatory) + (while (vectorp (car arg-list)) + (pop arg-list)) + ;; The next entry must be a mandatory arg. If we're + ;; looking for the first mandatory argument, just pick the + ;; first element. Otherwise loop further over the list and + ;; count for the correct arg: + (if (= mand-num 1) + (setq result (car arg-list)) + (while again + (cond ((vectorp (car arg-list)) + (pop arg-list) + (setq again t)) + ((= (cl-incf cnt) mand-num) + (setq again nil) + (setq result (car arg-list))) + (t + ;; Be a little conservative against infloops. + (if arg-list + (progn (setq again t) + (pop arg-list)) + (setq again nil))))))) + + ;; Optional arg after mandatory one(s): This isn't fun :-( + ((and (eq type 'optional) + (/= opt-dis 0)) + (setq again t) + (setq cnt 0) + ;; The idea is: Look for non-vectors and count the number + ;; of mandatory argument in `mand-num'. + (while again + (cond ((and (not (vectorp (car arg-list))) + (/= (cl-incf cnt) mand-num)) + (pop arg-list) + (setq again t)) + ((and (not (vectorp (car arg-list))) + ;; Don't incf mand-num again; is done in the + ;; clause above: + (= cnt mand-num)) + (setq again nil)) + ;; If the clauses above fail, we can safely drop + ;; vectors: + ((vectorp (car arg-list)) + (pop arg-list) + (setq again t)) + (t + (setq again nil)))) + (setq result (nth opt-dis arg-list))) + (t nil)) + result)) + +(defvar LaTeX-completion-function-map-alist-keyval + '((LaTeX-enumitem-env-with-opts . LaTeX-enumitem-key-val-options)) + "Alist mapping style funcs to completion-candidates counterparts. +Each element is a cons with the name of the function used in an +AUCTeX style file which queries and inserts something in the +buffer as car and the name function delievering completion +candidates as cdr. This list contains only mapping for functions +which perform key=val completions. See also +`LaTeX-completion-function-map-alist-cr'.") + +(defvar LaTeX-completion-function-map-alist-cr + '((TeX-arg-counter . LaTeX-counter-list) + (TeX-arg-pagestyle . LaTeX-pagestyle-list) ) + "Alist mapping style funcs to completion-candidates counterparts. +Each element is a cons with the name of the function used in an +AUCTeX style file which queries and inserts something in the +buffer as car and the name function delievering completion +candidates as cdr. This list contains only mapping for functions +which perform completing-read. See also +`LaTeX-completion-function-map-alist-keyval'.") + +(defun LaTeX-completion-parse-arg (arg) + "Parse ARG and call the correct candidates completion function. +ARG is the entry for the current argument in buffer stored in +`TeX-symbol-list' or `LaTeX-environment-list'." + (when (or (and (vectorp arg) + (symbolp (elt arg 0)) + (fboundp (elt arg 0))) + (and (listp arg) + (symbolp (car arg)) + (fboundp (car arg))) + (and (symbolp arg) + (fboundp arg))) + ;; Turn a vector into a list: + (when (vectorp arg) + (setq arg (append arg nil))) + ;; Turn a single function symbol into a list: + (unless (listp arg) + (setq arg (list arg))) + (let* ((head (car arg)) + (tail (cadr arg)) + (fun1 (lambda (elt) + (cond ((and (listp elt) + (symbolp (car elt)) + (fboundp (car elt))) + ;; It is a function call: + (funcall (car elt))) + ;; It is a function object + ((functionp elt) + (funcall elt)) + ;; It is a variable name + ((and (symbolp elt) + (boundp elt)) + (symbol-value elt)) + ;; It is a plain list of strings: + (t elt))))) + (cond ((eq head #'TeX-arg-key-val) + (LaTeX-completion-candidates-key-val + (funcall fun1 tail))) + + ((eq head #'TeX-arg-completing-read-multiple) + (LaTeX-completion-candidates-completing-read-multiple + (funcall fun1 tail))) + + ((eq head #'TeX-arg-completing-read) + (LaTeX-completion-candidates-completing-read + (funcall fun1 tail))) + + ((assq head LaTeX-completion-function-map-alist-keyval) + (LaTeX-completion-candidates-key-val + (funcall fun1 (cdr (assq head LaTeX-completion-function-map-alist-keyval))))) + + ((assq head LaTeX-completion-function-map-alist-cr) + (LaTeX-completion-candidates-completing-read + (funcall fun1 (cdr (assq head LaTeX-completion-function-map-alist-cr))))) + + (t nil))))) + +(defun LaTeX-completion-find-argument-boundries (&rest args) + "Find the boundries of the current LaTeX argument. +ARGS are characters passed to the function +`TeX-search-syntax-table'. If ARGS are omitted, all characters +defined in the variable `LaTeX-completion-macro-delimiters' are +taken." + (save-restriction + (narrow-to-region (line-beginning-position -20) + (line-beginning-position 20)) + (let ((args (or args (LaTeX-completion-macro-delimiters)))) + (condition-case nil + (with-syntax-table (apply #'TeX-search-syntax-table args) + (scan-lists (point) 1 1)) + (error nil))))) + +(defun LaTeX--arguments-completion-at-point () + "Capf for arguments of LaTeX macros and environments. +Completion for macros starting with `\\' is provided by the +function `TeX--completion-at-point' which should come first in +`completion-at-point-functions'." + ;; Exit if not inside an argument or in a comment: + (when (and (LaTeX-completion-find-argument-boundries) + (not (nth 4 (syntax-ppss)))) + (let ((entry (LaTeX-what-macro))) + (cond ((or (and entry + (eq (nth 1 entry) 'mac) + (assoc (car entry) (TeX-symbol-list))) + (and entry + (eq (nth 1 entry) 'env) + (assoc (car entry) (LaTeX-environment-list)))) + (LaTeX-completion-parse-arg + (LaTeX-completion-parse-args entry))) + ;; Any other constructs? + (t nil))))) + ;;; Mode (defgroup LaTeX-macro nil @@ -7419,6 +7884,11 @@ function would return non-nil and `(match-string 1)' would return (LaTeX-indent-commands-regexp-make) + ;; Standard Emacs completion-at-point support. We append the entry + ;; in order to let `TeX--completion-at-point' be first in the list: + (add-hook 'completion-at-point-functions + #'LaTeX--arguments-completion-at-point t t) + (set (make-local-variable 'LaTeX-item-list) '(("description" . LaTeX-item-argument) ("thebibliography" . LaTeX-item-bib) ("array" . LaTeX-item-array)