6

I'm trying to write a major-mode that highlights triple-quoted strings. Here's a minimal reproducible example:

(defconst demo-triple-quoted-string-regex
  (rx "\"\"\""
      ;; After the delimiter, we're a sequence of
      ;; non-backslashes or blackslashes paired with something.
      (*? (or (not (any "\\"))
              (seq "\\" anything)))
      "\"\"\""))

(defun demo-stringify-triple-quote ()
  "Put `syntax-table' property on triple-quoted strings."
  (let* ((string-literal (match-string 0))
         (string-start-pos (- (point) (length string-literal)))
         (string-end-pos (point)))
    (unless (nth 4 (syntax-ppss)) ;; not inside comment
      (put-text-property string-start-pos string-end-pos
                         'syntax-table (string-to-syntax "|")))))

(defconst demo-syntax-propertize-function
  (syntax-propertize-rules
   (demo-triple-quoted-string-regex
    (0 (ignore (demo-stringify-triple-quote))))))

(define-derived-mode demo-mode prog-mode "Demo"
  "Major mode showing stack overflow question."
  (set (make-local-variable 'font-lock-defaults) '(()))
  (set (make-local-variable 'syntax-propertize-function)
       demo-syntax-propertize-function))

However, this leads to really bizarre behaviour when modifying the buffer. Here's my buffer contents:

dodgy when we put a newline after babel

"""
a
"
babel

"""

x = 1

M-x demo-mode gives correct highlighting:

demo-before

but pressing enter suddenly gives this:

demo-after

What am I doing wrong?

4
  • I don't have a solution but I noticed the same problem with continuation of italics and bold across line wraps in org-mode.
    – Emacs User
    Commented Jun 21, 2015 at 22:08
  • 1
    The first problem is that you're putting the syntax on all characters of the string, while it should only be done for the first and last fence characters. You can verify that Emacs treats every pair of your supposed string as one sexp, i.e. single string, via forward-sexp.
    – politza
    Commented Jun 23, 2015 at 19:37
  • 1
    Second problem is that you can't really match strings the way you're imagine. This would only work, if the search is guarantied to start outside of any string already present in the buffer. After all you're matching pairs: One triple starts a string if and only if it is preceded by an even number of other triples. Luckily syntax-ppss keeps track of this. Take a look how it's done in python.el.
    – politza
    Commented Jun 23, 2015 at 19:38
  • @politza I am in awe of your elisp skills! Thanks so much :). Your corrections were enough for me to get my code working (see answer below), so I can now fix the julia-mode bug that got me into this. Commented Jun 23, 2015 at 22:00

1 Answer 1

3

Thanks to Politza, and stepping through python-syntax-stringify with edebug, I've got this working. The changes were:

  • | should only be applied to the first and last character in the triple-quoted-string.
  • (Because syntax parsing is incremental?) It's not possible to search for a whole string. Instead, search for a delimiter and see whether or not you're in a triple-quoted string.

Working code:

(defconst demo-triple-quoted-string-regex
  (rx "\"\"\""))

(defun demo-stringify-triple-quote ()
  "Put `syntax-table' property on triple-quoted strings."
  (let* ((string-end-pos (point))
         (string-start-pos (- string-end-pos 3))
         (ppss (prog2
                   (backward-char 3)
                   (syntax-ppss)
                 (forward-char 3))))
    (unless (nth 4 (syntax-ppss)) ;; not inside comment
      (if (nth 8 (syntax-ppss))
          ;; We're in a string, so this must be the closing triple-quote.
          ;; Put | on the last " character.
          (put-text-property (1- string-end-pos) string-end-pos
                             'syntax-table (string-to-syntax "|"))
        ;; We're not in a string, so this is the opening triple-quote.
        ;; Put | on the first " character.
        (put-text-property string-start-pos (1+ string-start-pos)
                           'syntax-table (string-to-syntax "|"))))))

(defconst demo-syntax-propertize-function
  (syntax-propertize-rules
   (demo-triple-quoted-string-regex
    (0 (ignore (demo-stringify-triple-quote))))))

(define-derived-mode demo-mode prog-mode "Demo"
  "Major mode showing stack overflow question."
  (set (make-local-variable 'font-lock-defaults) '(()))
  (set (make-local-variable 'syntax-propertize-function)
       demo-syntax-propertize-function))
1
  • Why do you compute ppss in the let* and then never use that value? Commented Oct 31, 2019 at 10:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.