Some remarks at first:
- This question should not only be about finding the end of a string. Also finding the beginning of a string is quite complicated if you want to do it right. There may be double-quote characters
?\"
within the program text that must be ignored. Furthermore there may be single double-quote characters within comments that also must be ignored.
- Don't use text properties since these depend on the buffer fontification. If the buffer is lazily fontified it is not fully fontified. You would need to call
font-lock-ensure
. This is often too much work for the job to be done.
If the major mode uses the syntax table to identify comments and strings it is better to use parse-partial-sexp
.
I demonstrate this with the following elisp snippet.
You can use the macro for-the-strings
for your purpose.
Just implement your wanted functionality as the body of this macro.
The first two args are symbols that are bound to the beginning and the end of the string. The third arg is the end of the region to search for strings.
If you set the third arg to nil the buffer is searched up to (point-max)
.
(defmacro for-the-strings (b e end &rest body)
"For each string found in current buffer starting at `point' and not further than END
`let'-bind B and E to the beginning and end, resp., of the string (inclusively string delimiters)
and execute BODY.
Note that you may modify the string within BODY but you may not remove the string delimiters and you may not move the start of the string.
You don't need to preserve point but you may not change the value of B within BODY."
(declare (debug (symbolp symbolp form body)) (indent 3))
(let ((sym-end (make-symbol "end"))) ;; generate new uninterned symbol to avoid clash with symbol `end' which maybe already in the namespace.
`(let (ppss
(,sym-end (make-marker)))
(unwind-protect
(progn
(set-marker ,sym-end (or ,end (point-max)))
(while (progn
;; SEARCH FOR THE NEXT STRING OR COMMENT:
(setq ppss (parse-partial-sexp (point) ,sym-end nil nil ppss 'syntax-table))
(and (< (point) ,sym-end)
(null (eobp))))
(when (nth 3 ppss) ;; NOT A COMMENT BUT A STRING
(backward-char) ;; NOW WE ARE AT THE STRING STARTER
(let ((,b (point))
(,e (scan-sexps (point) 1))) ;; FIND STRING END
,@body
(goto-char (1+ ,b)) ;; Maybe, the string is modified by side-effects.
;; The safest action is to start again from the beginning of the string position.
;; We skip to the start of the next sexp.
;; That may also be a string or a comment.
(setq ppss (parse-partial-sexp (point) ,sym-end nil t ppss))
))))
(set-marker ,sym-end nil)
))))
;; Demo:
(let* ((end (save-excursion (search-forward "ENDMARK" nil t))))
(search-forward "TESTDATA" nil t 2) ;; We start with the search at the string TESTDATA below.
(for-the-strings
b
e
end
(message "String: %s" (buffer-substring-no-properties b e))
;; Next we demonstrate how we can search and modify the found string:
(goto-char b)
(when (search-forward "FOOBAR" e t)
(replace-match "FOO")))) ;;< Evaluate the demo form at that closing paren with C-x C-e and observe the emitted messages.
;; TESTDATA:
"This string is shown.
Escaped \"double-quotes\" work fine." ; some comment
"FOOBAR is replaced by FOO"
(setq some-form ; another comment
'(1
2
?\" ;;< This escaped double-quote does not start a string.
"String after ?\\\"."))
; ENDMARK
(message "This string is ignored")