Subject: 29.0.50; [PATCH] Xref backend for TeX buffers
From: David Fussner <[email-masked]>
To: [email-masked]
Date: Thu, 3 Feb 2022 15:09:22 +0000
List-Id: <debbugs-submit.debbugs.gnu.org>

I've recently been trying to use xref commands with a tags table in a
TeX repository, and many of the results are sub-optimal.  This is a
known issue -- within living memory there have been at least two
discussions related to it on help-gnu-emacs:

https://lists.gnu.org/archive/html/help-gnu-emacs/2018-06/msg00126.html
https://lists.gnu.org/archive/html/help-gnu-emacs/2021-07/msg00436.html

Neither discussion resulted in any code, at least not that I can find,
and the issues mentioned there remain.  For example,
xref-find-definitions on, say, '\mycommand' returns

No definitions found for: mycommand.

(The absence of the escape char in the search string makes the search
fail, as the tag name in the table will be '\mycommand'.)

Similarly, any xref command on 'my:citekey' will only search by default
for the half of the symbol under point, stopping at the colon.

There are many other behaviors that are suboptimal, as well, so in the
end I wrote a new xref backend for TeX buffers (cloning large portions
of the default etags backend), and wondered whether it might be welcome
in GNU Emacs.

A few remarks:

1. The code should work as it stands both in the AUCTeX and the in-tree
modes.  The AUCTeX hooks I've included in the patch are provisional, as
I would want to discuss with them how they would want to handle it,
should the patch be accepted in some form.

2. Along the way I found some issues with how etags parses TeX files,
issues which affect the usefulness of the xref commands, so I've made
changes in etags.c as well.  When running the test suite for etags the
only diffs occurred in the TeX-related sections of the resulting tags
file, and location information in those sections was good.

3. The patch as it stands enables all the changes by default to give
what I judge to be the best out-of-the-box experience, but wiser heads
may well have other ideas.

4. If it looks like the patch will make it into Emacs in some form, I'm
going to need to assign copyright, so I'd appreciate help with getting
that started.

Thanks,

David.
From 9f5b2547fef5597f9c41e1c46f99746095a0834c Mon Sep 17 00:00:00 2001
From: David Fussner <[email-masked]>
Date: Wed, 2 Feb 2022 13:31:51 +0000
Subject: [PATCH] Provide an xref backend for TeX buffers

* lib-src/etags.c (TeX_commands): Improve parsing of commands in TeX
buffers.
(TEX_defenv): Expand list of commands to tag by default in TeX
buffers.
(TeX_help):
* doc/emacs/maintaining.texi (Tag Syntax): Document new tagged
commands.

* lisp/textmodes/tex-mode.el (tex--xref-backend): New function to name
xref backend.
(tex-common-initialization): Set up xref backend for in-tree TeX
modes.
(tex-set-auctex-xref-backend): New function to do the same for AUCTeX
modes.
(xref-backend-identifier-at-point)
(xref-backend-identifier-completion-table)
(xref-backend-identifier-completion-ignore-case)
(xref-backend-definitions, xref-backend-apropos)
(xref-backend-references): New TeX implementations of the generic xref
backend functions.
(tex-xref-apropos-regexp, tex-xref-references-in-directory): New
helper functions for backend.
(tex-thingatpt-modes-list): New var.
(tex-thingatpt-is-texsymbol): New defcustom.
(tex-set-thingatpt-symbol): New command to apply value of previous
buffer-locally.
(tex--symbol-or-texsymbol): New helper function for previous.
(tex-thingatpt--beginning-of-texsymbol)
(tex-thingatpt--end-of-texsymbol): New functions to define texsymbol
"thing" for 'thing-at-point'.
(tex-thingatpt-syntax-table, tex-escape-char): New vars to do the
same.
(tex--thing-at-point): New function to return texsymbol
'thing-at-point'.
(tex-thingatpt-include-escape, tex-xref-try-alternate-forms): New
defcustoms to refine behavior of the xref backend.
(tex--include-escape-p): New function to do the same.
---
 doc/emacs/maintaining.texi |   9 +-
 lib-src/etags.c            |  83 ++++++++--
 lisp/textmodes/tex-mode.el | 331 +++++++++++++++++++++++++++++++++++++
 3 files changed, 411 insertions(+), 12 deletions(-)

diff --git a/doc/emacs/maintaining.texi b/doc/emacs/maintaining.texi
index edcc6075f7..1de435246e 100644
--- a/doc/emacs/maintaining.texi
+++ b/doc/emacs/maintaining.texi
@@ -2565,8 +2565,13 @@ Tag Syntax
 @code{\section}, @code{\subsection}, @code{\subsubsection},
 @code{\eqno}, @code{\label}, @code{\ref}, @code{\cite},
 @code{\bibitem}, @code{\part}, @code{\appendix}, @code{\entry},
-@code{\index}, @code{\def}, @code{\newcommand}, @code{\renewcommand},
-@code{\newenvironment} and @code{\renewenvironment} are tags.
+@code{\index}, @code{\def}, @code{\edef}, @code{\gdef}, @code{\xdef},
+@code{\newcommand}, @code{\renewcommand}, @code{\newenvironment},
+@code{\renewenvironment}, @code{\DeclareRobustCommand},
+@code{\newrobustcmd}, @code{\renewrobustcmd}, @code{\let},
+@code{\csdef}, @code{\csedef}, @code{\csgdef}, @code{\csxdef},
+@code{\csletcs}, and @code{\cslet} are tags.  So too are the arguments
+of any starred variants of these commands, when such variants exist.
 
 Other commands can make tags as well, if you specify them in the
 environment variable @env{TEXTAGS} before invoking @command{etags}.  The
diff --git a/lib-src/etags.c b/lib-src/etags.c
index aa5bc8839d..e5269aa456 100644
--- a/lib-src/etags.c
+++ b/lib-src/etags.c
@@ -793,8 +793,12 @@ #define STDIN 0x1001		/* returned by getopt_long on --parse-stdin */
 "In LaTeX text, the argument of any of the commands '\\chapter',\n\
 '\\section', '\\subsection', '\\subsubsection', '\\eqno', '\\label',\n\
 '\\ref', '\\cite', '\\bibitem', '\\part', '\\appendix', '\\entry',\n\
-'\\index', '\\def', '\\newcommand', '\\renewcommand',\n\
-'\\newenvironment' or '\\renewenvironment' is a tag.\n\
+'\\index', '\\def', '\\edef', '\\gdef', '\\xdef', '\\newcommand',\n\
+'\\renewcommand', '\\newenvironment', '\\renewenvironment',\n\
+'\\DeclareRobustCommand, '\\newrobustcmd', '\\renewrobustcmd',\n\
+'\\let', '\\csdef', '\\csedef', '\\csgdef', '\\csxdef', '\\csletcs',\n\
+or '\\cslet' is a tag.  So is the argument of any of the starred\n\
+variants of these commands, when a starred variant exists.\n\
 \n\
 Other commands can be specified by setting the environment variable\n\
 'TEXTAGS' to a colon-separated list like, for example,\n\
@@ -5673,11 +5677,19 @@ Scheme_functions (FILE *inf)
 static linebuffer *TEX_toktab = NULL; /* Table with tag tokens */
 
 /* Default set of control sequences to put into TEX_toktab.
-   The value of environment var TEXTAGS is prepended to this.  */
+   The value of environment var TEXTAGS is prepended to this.
+   (2021) Add variants of '\def', some additional LaTeX commands,
+   and common variants from the 'etoolbox' package.  Also, add
+   starred variants of the commands if they exist.  Starred
+   variants need to appear before their unstarred versions. */
 static const char *TEX_defenv = "\
-:chapter:section:subsection:subsubsection:eqno:label:ref:cite:bibitem\
-:part:appendix:entry:index:def\
-:newcommand:renewcommand:newenvironment:renewenvironment";
+:chapter*:section*:subsection*:subsubsection*:part*:label:ref\
+:chapter:section:subsection:subsubsection:eqno:cite:bibitem\
+:part:appendix:entry:index:def:edef:gdef:xdef:newcommand*:newcommand\
+:renewcommand*:renewcommand:newenvironment*:newenvironment\
+:renewenvironment*:renewenvironment:DeclareRobustCommand*\
+:DeclareRobustCommand:renewrobustcmd*:renewrobustcmd:newrobustcmd*\
+:newrobustcmd:let:csdef:csedef:csgdef:csxdef:csletcs:cslet";
 
 static void TEX_decode_env (const char *, const char *);
 
@@ -5736,19 +5748,70 @@ TeX_commands (FILE *inf)
 	      {
 		char *p;
 		ptrdiff_t namelen, linelen;
-		bool opgrp = false;
+		bool opgrp = false, one_esc = false;
 
 		cp = skip_spaces (cp + key->len);
+		/* Skip the optional arguments to commands in the tags list so
+		   that these arguments don't end up as the name of the tag.
+		   The name will instead come from the argument in curly braces
+		   that follows the optional ones.  */
+		if (*cp == '[' || *cp == '(')
+		  {
+		    while (*cp != TEX_opgrp && *cp != '\0')
+		      cp++;
+		  }
 		if (*cp == TEX_opgrp)
 		  {
 		    opgrp = true;
 		    cp++;
 		  }
+		/* Jumping to a TeX command definition doesn't work in at
+		   least some of the editors that use ctags.  Changes in
+		   tex-mode.el in GNU Emacs address these issues for etags;
+		   uncomment the following five lines to get a quick & dirty
+		   improvement in programs using ctags as well, though some
+		   parts of the behavior will remain suboptimal.  The
+		   undocumented ctags option '--no-duplicates' may help.  */
+
+		/* if (CTAGS && *cp == TEX_esc) */
+		/*   { */
+		/*     cp++; */
+		/*     one_esc = true; */
+		/*   } */
+
+		/* Add optional argument brackets '(' and '[' so that these
+		   arguments don't appear in tag names.  Also add '=' as it's
+		   relational in the vast majority of cases.  */
 		for (p = cp;
-		     (!c_isspace (*p) && *p != '#' &&
-		      *p != TEX_opgrp && *p != TEX_clgrp);
+		     (!c_isspace (*p) && *p != '#' && *p != '=' &&
+		      *p != '[' && *p != '(' && *p != TEX_opgrp &&
+		      *p != TEX_clgrp);
 		     p++)
-		  continue;
+		  /* Allow only one escape char in a tag name, which
+		     (primarily) enables tagging a TeX command's different,
+		     possibly temporary, '\let' bindings.  */
+		  if (*p == TEX_esc)
+		    {
+		      if (!one_esc)
+			{
+			  one_esc = true;
+			  continue;
+			}
+		      else
+			break;
+		    }
+		  else
+		    continue;
+		/* Re-scan to catch (highly unusual) cases where a
+		   command name is of the form '\('.  */
+		if ((*p == '(' || *p == '[') && (p - cp) < 2)
+		  {
+		    for (p = cp;
+			 (!c_isspace (*p) && *p != '#' &&
+			  *p != TEX_opgrp && *p != TEX_clgrp);
+			 p++)
+		      continue;
+		  }
 		namelen = p - cp;
 		linelen = lb.len;
 		if (!opgrp || *p == TEX_clgrp)
diff --git a/lisp/textmodes/tex-mode.el b/lisp/textmodes/tex-mode.el
index ab94036d01..3a7178c055 100644
--- a/lisp/textmodes/tex-mode.el
+++ b/lisp/textmodes/tex-mode.el
@@ -1291,6 +1291,9 @@ tex-common-initialization
 	      (syntax-propertize-rules latex-syntax-propertize-rules))
   ;; TABs in verbatim environments don't do what you think.
   (setq-local indent-tabs-mode nil)
+  ;; Set up xref backend in TeX buffers.
+  (add-hook 'xref-backend-functions #'tex--xref-backend nil t)
+  (tex-set-thingatpt-symbol)
   ;; Other vars that should be buffer-local.
   (make-local-variable 'tex-command)
   (make-local-variable 'tex-start-of-header)
@@ -3659,6 +3662,334 @@ tex-chktex
       (process-send-region tex-chktex--process (point-min) (point-max))
       (process-send-eof tex-chktex--process))))
 
+
+;;; Xref backend
+
+;; Here we define an xref backend for TeX, adapting the default etags
+;; backend so that the main xref user commands (including
+;; `xref-find-definitions', `xref-find-apropos', and
+;; `xref-find-references' [on M-., C-M-., and M-?, respectively]) work
+;; in TeX buffers.  This mostly involves defining a new THING for
+;; `thing-at-point' (texsymbol), then substituting that THING for
+;; `symbol' in TeX buffers, at least by (configurable) default.  The
+;; TeX escape character will by default appear in the resulting string
+;; only when the xref command uses string search and not regexp
+;; search, though this too is configurable.  The new THING type also
+;; improves the accuracy of other commands that use `thing-at-point'
+;; in TeX buffers, like `project-find-regexp'.  TODO: Include commands
+;; that call `bounds-of-thing-at-point' (for example
+;; `isearch-forward-thing-at-point') in the mechanism.
+
+(defvar tex-thingatpt-modes-list
+  '(tex-mode doctex-mode latex-mode plain-tex-mode slitex-mode)
+  "Major modes where `thing-at-point' may use the `texsymbol' type.
+
+When a buffer's `major-mode' is in this list, and when
+`tex-thingatpt-is-texsymbol' is t (the default), any command in
+that buffer that calls `thing-at-point' with a `symbol' argument
+actually uses the `texsymbol' argument, instead.")
+
+(defcustom tex-thingatpt-is-texsymbol t
+  "When non-nil replace `symbol' by `texsymbol' for `thing-at-point'.
+
+This applies only to TeX buffers.  The `texsymbol' \"thing\"
+modifies the standard `symbol' for use in such buffers.
+
+When nil, restore the default behavior of `thing-at-point' in TeX
+buffers.
+
+Custom will automatically apply changes in all TeX buffers, but
+if you set the variable outside of Custom it won't take effect
+until you apply it with \\[tex-set-thingatpt-symbol].  Without a
+prefix argument (\\[universal-argument]) this applies only to the
+current buffer, but with one it applies to all TeX buffers in
+`buffer-list'.  (TeX buffers are those whose `major-mode' is a
+member of `tex-thingatpt-modes-list'.)"
+  :type 'boolean
+  :group 'tex-file
+  :initialize #'custom-initialize-default
+  :set (lambda (var val)
+         (set-default var val)
+         (tex-set-thingatpt-symbol t))
+  :version "29.1")
+
+(defcustom tex-thingatpt-include-escape '(xref-find-definitions
+                                          xref-find-definitions-other-window
+                                          xref-find-definitions-other-frame)
+  "If non-nil, include `tex-escape-char' in `thing-at-point'.
+
+This variable only takes effect when `tex-thingatpt-is-texsymbol'
+is t (the default), changing the argument passed to
+`thing-at-point' from `symbol' to `texsymbol'.  When that is the
+case, the values of this variable act as follows:
+
+When t, `thing-at-point' will always include a
+`tex-escape-char' (usually `\\'), should one be present, in the
+string it returns in TeX buffers.
+
+When nil, `thing-at-point' will never include the
+`tex-escape-char' in the string it returns in TeX buffers.
+
+Otherwise, it's a list of commands for which `thing-at-point'
+will always include the `tex-escape-char' in the string it
+returns.  The three xref commands listed by default may cease to
+function properly in TeX buffers if set to nil, but setting
+`tex-xref-try-alternate-forms' to t will rectify that."
+  :type '(choice (const :tag "Always include tex-escape-char" t)
+                 (const :tag "Never include tex-escape-char" nil)
+                 (set :tag "Include tex-escape-char for these commands"
+		      (repeat :inline t (symbol :tag "command"))))
+  :group 'tex-file
+  :version "29.1")
+
+(defcustom tex-xref-try-alternate-forms nil
+  "Non-nil means find definitions of alternate forms of commands.
+
+If `xref-find-definitions' returns nil for the current form of
+the TeX command name, try the alternative form, which will have
+the `tex-escape-char' (usually `\\') either stripped from or
+prepended to the current form, depending on whether or not the
+current form starts with that character.
+
+This may be particularly useful in documents that mix `\\def' and
+`\\csdef' when defining commands."
+  :type 'boolean
+  :group 'tex-file
+  :version "29.1")
+
+(defvar tex-escape-char ?\\
+  "The current TeX escape character.
+
+The `etags' program only recognizes `\\' (92) and `!' (33) as
+escape characters in TeX documents, and if it detects the latter
+it also uses `<>' as the TeX grouping construct rather than `{}'.
+Setting this variable to anything other than `\\' or `!' will not
+be useful without changes to `etags', at least for commands that
+search tags tables, such as \\[xref-find-definitions] and \
+\\[xref-find-apropos].")
+
+(defvar tex-thingatpt-syntax-table
+  (let* ((ost (if (boundp 'TeX-mode-syntax-table)
+                  TeX-mode-syntax-table
+                tex-mode-syntax-table))
+         (st (make-syntax-table ost)))
+    (modify-syntax-entry ?# "'" st)
+    (modify-syntax-entry ?= "'" st)
+    (modify-syntax-entry ?` "'" st)
+    (modify-syntax-entry ?\" "'" st)
+    (modify-syntax-entry ?' "'" st)
+    st)
+  "Syntax table for delimiting `thing-at-point' in TeX buffers.
+
+When `tex-thingatpt-is-texsymbol' is t, this syntax table helps
+to define what a `texsymbol' is.")
+
+(defun tex--xref-backend () 'tex)
+
+;; Setup AUCTeX modes.  (Should this be in AUCTeX itself?)
+
+(add-hook 'TeX-mode-hook #'tex-set-auctex-xref-backend)
+(add-hook 'TeX-mode-hook #'tex-set-thingatpt-symbol)
+
+(defun tex-set-auctex-xref-backend ()
+  (add-hook 'xref-backend-functions #'tex--xref-backend nil t))
+
+(declare-function xref-item-location "xref")
+(declare-function xref--project-root "xref" (project))
+(declare-function xref--convert-hits "xref" (hits regexp))
+(declare-function apropos-parse-pattern "apropos" (pattern))
+(declare-function semantic-symref-perform-search "semantic/symref")
+(declare-function semantic-symref-instantiate "semantic/symref")
+(declare-function project-external-roots "project")
+(declare-function find-tag--completion-ignore-case "etags")
+(declare-function etags--xref-find-definitions "etags")
+(declare-function etags--xref-apropos-additional "etags" (regexp))
+(declare-function cl-delete-if "cl-seq")
+(defvar etags-xref-prefer-current-file)
+
+(cl-defmethod xref-backend-identifier-at-point ((_backend (eql 'tex)))
+  (require 'etags)
+  (thing-at-point 'symbol t))
+
+(cl-defmethod xref-backend-identifier-completion-table ((_backend
+                                                         (eql 'tex)))
+  (tags-lazy-completion-table))
+
+(cl-defmethod xref-backend-identifier-completion-ignore-case ((_backend
+                                                               (eql 'tex)))
+  (find-tag--completion-ignore-case))
+
+(cl-defmethod xref-backend-definitions ((_backend (eql 'tex)) symbol)
+  (let* ((file (and buffer-file-name (expand-file-name buffer-file-name)))
+         (alt-sym (if (char-equal tex-escape-char (aref symbol 0))
+                      (substring symbol 1)
+                    (concat (string tex-escape-char) symbol)))
+         (prelim-definitions (etags--xref-find-definitions symbol))
+         (definitions (if (or prelim-definitions
+                              (not tex-xref-try-alternate-forms))
+                          prelim-definitions
+                        (etags--xref-find-definitions alt-sym)))
+         same-file-definitions)
+    (when (and etags-xref-prefer-current-file file)
+      (setq definitions
+            (cl-delete-if
+             (lambda (definition)
+               (when (equal file
+                            (xref-location-group
+                             (xref-item-location definition)))
+                 (push definition same-file-definitions)
+                 t))
+             definitions))
+      (setq definitions (nconc (nreverse same-file-definitions)
+                               definitions)))
+    definitions))
+
+(cl-defmethod xref-backend-apropos ((_backend (eql 'tex)) pattern)
+  (let ((regexp (tex-xref-apropos-regexp pattern)))
+    (nconc
+     (or
+      (etags--xref-find-definitions regexp t)
+      (etags--xref-find-definitions pattern t))
+     (etags--xref-apropos-additional regexp))))
+
+(cl-defmethod xref-backend-references ((_backend (eql 'tex)) identifier)
+  (mapcan
+   (lambda (dir)
+     (message "Searching %s..." dir)
+     (redisplay)
+     (prog1
+         (tex-xref-references-in-directory identifier dir)
+       (message "Searching %s... done" dir)))
+   (let ((pr (project-current t)))
+     (cons
+      (xref--project-root pr)
+      (project-external-roots pr)))))
+
+(defun tex-xref-apropos-regexp (pattern)
+  "Return a regexp from PATTERN similar to `apropos'.
+
+Unlike the standard xref function, if `regexp-quote' returns a
+string different from the original PATTERN, the TeX function
+passes that modified string, rather than PATTERN itself, to
+`apropos-parse-pattern'."
+  (let ((re (regexp-quote pattern)))
+    (apropos-parse-pattern
+     (if (string-equal re pattern)
+         ;; Split into words
+         (or (split-string pattern "[ \t]+" t)
+             (user-error "No word list given"))
+       re))))
+
+(defun tex-xref-references-in-directory (symbol dir)
+  "Find all references to SYMBOL in directory DIR.
+Return a list of xref values.
+
+This function uses the Semantic Symbol Reference API.  In TeX
+buffers the value returned when passing SYMBOL to `regexp-quote'
+becomes the default search term.  If this symref instantiation
+finds no matches, a second tries again with the original SYMBOL
+as search term, instead.  Both searches set keyword `searchtype:'
+to \\='regexp instead of xref's \\='symbol.
+
+See `semantic-symref-tool-alist' for details on which tools are
+used, and when.  See also `xref-references-in-directory' and
+comments in its code, the latter copied into the TeX
+implementation for convenience."
+  (cl-assert (directory-name-p dir))
+  (require 'semantic/symref)
+  (defvar semantic-symref-tool)
+  (defvar ede-minor-mode)
+
+  ;; Some symref backends use `ede-project-root-directory' as the root
+  ;; directory for the search, rather than `default-directory'. Since
+  ;; the caller has specified `dir', we bind `ede-minor-mode' to nil
+  ;; to force the backend to use `default-directory'.
+  (let* ((ede-minor-mode nil)
+         (default-directory dir)
+         ;; FIXME: Remove CScope and Global from the recognized tools?
+         ;; The current implementations interpret the symbol search as
+         ;; "find all calls to the given function", but not function
+         ;; definition. And they return nothing when passed a variable
+         ;; name, even a global one.
+         (semantic-symref-tool 'detect)
+         (case-fold-search nil)
+         (texsymbol (regexp-quote symbol))
+         (inst (semantic-symref-instantiate :searchfor texsymbol
+                                            :searchtype 'regexp
+                                            :searchscope 'subdirs
+                                            :resulttype 'line-and-text))
+         (alt-inst (semantic-symref-instantiate :searchfor symbol
+                                                :searchtype 'regexp
+                                                :searchscope 'subdirs
+                                                :resulttype 'line-and-text)))
+    (or
+     (xref--convert-hits (semantic-symref-perform-search inst)
+                         (format "%s" texsymbol))
+     (xref--convert-hits (semantic-symref-perform-search alt-inst)
+                         (format "%s" symbol)))))
+
+(put 'texsymbol 'beginning-op 'tex-thingatpt--beginning-of-texsymbol)
+
+(put 'texsymbol 'end-op 'tex-thingatpt--end-of-texsymbol)
+
+(defun tex-set-thingatpt-symbol (&optional all)
+  "Set meaning of `thing-at-point' `symbol' in (ALL?) TeX buffers.
+
+When `tex-thingatpt-is-texsymbol' is t, set `thing-at-point' to
+use the `texsymbol' \"thing\" instead of `symbol', otherwise
+maintain or restore the default.  Without an optional ALL make
+changes only in current buffer, with ALL make changes in all TeX
+buffers in `buffer-list'."
+  (interactive "P")
+  (require 'thingatpt)
+  (if all
+      (dolist (buf (buffer-list))
+        (with-current-buffer buf
+          (tex--symbol-or-texsymbol)))
+    (tex--symbol-or-texsymbol)))
+
+(defun tex--symbol-or-texsymbol ()
+  (when (memq major-mode tex-thingatpt-modes-list)
+    (if tex-thingatpt-is-texsymbol
+        (setq-local thing-at-point-provider-alist
+                    (add-to-list 'thing-at-point-provider-alist
+                            '(symbol . tex--thing-at-point)))
+      (setq-local thing-at-point-provider-alist
+                  (delete '(symbol . tex--thing-at-point)
+                          thing-at-point-provider-alist)))))
+
+(defun tex--thing-at-point ()
+  "Pass `thing' type `texsymbol' to `bounds-of-thing-at-point'.
+
+When `tex-thingatpt-is-texsymbol' is t, calls in TeX buffers to
+`thing-at-point' with argument `symbol' will use this function."
+  (let* ((sytab (make-syntax-table tex-thingatpt-syntax-table))
+         (bounds (with-syntax-table sytab
+                   (unless (char-equal tex-escape-char ?\\)
+                     (modify-syntax-entry ?\\ "_")
+                     (modify-syntax-entry tex-escape-char "\\")
+                     (modify-syntax-entry ?< "(>")
+                     (modify-syntax-entry ?> ")<"))
+                   (bounds-of-thing-at-point 'texsymbol))))
+    (when bounds
+      (buffer-substring-no-properties (car bounds) (cdr bounds)))))
+
+(defun tex--include-escape-p (command)
+  (or (eq tex-thingatpt-include-escape t)
+      (memq command tex-thingatpt-include-escape)))
+
+(defun tex-thingatpt--beginning-of-texsymbol ()
+  "Move point to the beginning of the current TeX symbol."
+  (and (re-search-backward "\\([][()]\\|\\(\\sw\\|\\s_\\|\\s.\\)+\\)")
+       (skip-syntax-backward "w_.")
+       (when (tex--include-escape-p this-command)
+         (skip-syntax-backward "\\/"))))
+
+(defun tex-thingatpt--end-of-texsymbol ()
+  "Move point to the end of the current TeX symbol."
+  (and (re-search-forward "\\([][()]\\|\\(\\sw\\|\\s_\\|\\s.\\)+\\)")
+       (skip-syntax-forward "w_.")))
+
 (make-obsolete-variable 'tex-mode-load-hook
                         "use `with-eval-after-load' instead." "28.1")
 (run-hooks 'tex-mode-load-hook)
-- 
2.17.6