StringAtPoint

Adventures in trying to determine a programming language string under point using the ThingAtPoint library that comes with Emacs.

Searching for a string is not trivial, because of the possibility of escaped " and the arbitrary quality of where the point could be in a string. If you expect to return the string when point is somewhere in the middle of it then things are much more complicated than if you expect it to return the string only when point is at one of the quote marks.

The following commands are helpful for repetitive editing of strings in a programming language or a scripting language like shell.

Here’s a ‘forward-thing’ function for strings.

  (defun ash-forward-string (&optional arg)
    "Move forward to ARGth string."
    (setq arg (or arg 1))
    (if (not (bobp))
        (save-match-data
          (when (or (and (looking-at-p "\\s-*\"")
                         (not (looking-back "\\\\")))
                    (re-search-backward "[^\\\\]\"" nil nil))
            (looking-at "\\s-*\"")
            (goto-char (match-end 0))
            (forward-char -1))))
    (while (and (> arg 0)
                (not (eobp))
                (looking-at-p "\\s-*\""))
      (forward-sexp 1)
      (setq arg (1- arg)))
    (while (and (< arg 0)
                (not (bobp))
                (looking-at-p "\""))
      (forward-sexp -1)
      (setq arg (1+ arg)))
    (ignore))

Then, tell the thingatpt.el library about it.

  (put 'string 'forward-op 'ash-forward-string)

A kill command becomes possible with.

  (defun ash-kill-string (&optional arg) 
    "Kill ARG strings under point."
    (interactive "*p")
    (setq arg (or (and (not (zerop arg)) arg) 1))
    (if (> arg 0)
        (kill-region
         (progn (forward-thing 'string 0) (point))
         (progn (forward-thing 'string arg) (point)))
      (kill-region
       (progn (forward-thing 'string 1) (point))
       (progn (forward-thing 'string arg) (point)))))

Here are some tests.

  (defun ash-test-string-motion ()
    "Test `ash-forward-string'."
    (with-temp-buffer
      (insert "\"String0\"")
      (insert " \t ")
      (insert "\"\\\"String1\"")
      (newline)
      (insert " \"String2\"")
      (newline)
      (goto-char (point-min))
      (ash-forward-string 3) ;; Forward 3 strings.
      (unless (eq (1+ (point)) (point-max))
        (error "Test 1 failed"))
      (goto-char (point-min))
      (forward-line 1) ;; Start from escaped quotation mark.
      (forward-char 1)
      (ash-forward-string 2)
      (unless (eq (1+ (point)) (point-max))
        (error "Test 2 failed"))
      (goto-char (point-min))
      (re-search-forward "\t" nil t) ;; Start from whitespace.
      (ash-forward-string 2)
      (unless (eq (1+ (point)) (point-max))
        (error "Test 3 failed"))
      (goto-char (point-max))
      (forward-char -2) ;; Start on ending quotation mark.
      (ash-forward-string -3)
      (unless (looking-at-p "String2")
        (error "Test 4 failed"))
      (goto-char (point-max))
      (forward-char -3) ;; Start inside of ending quotation mark.
      (ash-forward-string -4)
      (unless (eq (point) (point-min))
        (error "Test 5 failed"))
      (message "Test succeeded")))

See also ‘forward-whitespace’, ‘forward-symbol’ and ‘parse-partial-sexp’.

Using Emacs built-in functions

[new]

I think a more reliable way to write functions searching for strings is using Emacs built-in functions for this. I have added functions for this to ourcomments-util.el in nXhtml. (This is yet just in the repository.)

Using this you can test for a string with (thing-at-point ‘string).

PS: I really appreciate when someone cleans up things here. But I would also be glad if those who delete things they do not understand asks before doing that. – LennartBorgman

[new] I pulled down your code and wrote the following motion command (i.e. `forward-thing) with it.

  (defun ourcomments-forward-string (&optional arg)
    "Move point forward ARG (backward if ARG is negative) string(s).
  Normally returns t if point moved, else nil."
    (interactive "p")
    (let ((arg (or arg 1)))
      (while (< arg 0)
        (beginning-of-thing 'string)
        (setq arg (1+ arg)))
      (while (> arg 0)
        (end-of-thing 'string)
        (setq arg (1- arg)))))
  (put 'string 'forward-op 'ourcomments-forward-string)

Then, when I ran it in my motion test suite, it hard failed on all the tests by failing to recognize a string when point is at the beginning of a string

and generally not being able to move across strings. It therefore doesn’t work with the generic ‘kill’ command defined above. (Writing something to move backwards across strings is maybe a bit complicated.)

When I tried a naive version of the kill command, it worked.

  (defun ourcomment-kill-string ()
    "Kill string under point."
    (interactive "*")
    (let ((string-bounds (bounds-of-thing-at-point 'string)))
      (unless (null string-bounds)
        (kill-region (car string-bounds)
                     (cdr string-bounds)))))

But only when the point is in the string, but not the beginning. – AaronHawley

[new] I have taken care of the end points in the new version in the nXhtml repository.

If you look at the code for beginning-of-thing it assumes that point is in “thing” so you can not use the function for ‘bounds-of-thing-at-point to move the way you want. – LennartBorgman

[new] Well, I took ‘forward-sexp’ and ‘kill-sexp’ as a starting point, since they can operate on strings pretty well, but then I took it to the next level – noting that strings can’t be nested like sexps are – allowing me to make the commands to work from inside a string.

How does your corrected code fair in the test suite? – AaronHawley

[new] Which test suite? – LennartBorgman

[new] The function defined above named ‘ash-test-string-motion’. It was useful to me for developing my version, but I’ve been meaning to write a new one that is more granular and can show the specific problems with an implementation. – AaronHawley

[new] I do not see how that test is related to the functions I wrote. Those functions are for (thing-at-point ‘string).

I did not write any functions for moving across strings (though that may be possible too, using parse-partial-sexp).


CategoryCode