8

I've written a simple mode for handling JSON. It uses the derived machinery to re-use most of json-mode's code. However one addition is you can insert elisp into the JSON text which is evaluated at JSON submission time. For example an excerpt of the json looks like this:

{
    "parameters": {
        "IRC_USER": "stsquad",
        "PUB_KEY": `(format "\"%s\"" (s-trim (shell-command-to-string "cat ~/.ssh/id_rsa.pub")))`
    }
}

Currently the syntax highlighting of this text is broken as the JSON syntax hightlighter get's thrown by the elisp. I'd like to set-up a nested syntax table so the elisp is properly recognised as elisp when inside the escape characters (I've chosen ` in this case). I understand you can join char-tables (which syntax-tables are built from) with something like:

(defvar lava-mode-syntax-table
  (let ((json-table (copy-syntax-table json-mode-syntax-table))
        (elisp-table (copy-syntax-table lisp-mode-syntax-table)))
    (set-char-table-parent elisp-table json-table)
    (modify-syntax-entry ?` "(`" json-table)
    (modify-syntax-entry ?` ")`" json-table)
    json-table)
  "LAVA Mode syntax table.
This is a combination of json-mode-syntax-table with an escape into
  lisp-mode-syntax table for the embedded elisp.")

But I don't understand how I can modify the syntax table to start using the child (elisp) syntax table while between the escape characters?

stsquad
  • 4,626
  • 28
  • 45
  • 1
    Is syntax highlighting the only goal? If so, some smart font-lock rules might be a lot easier than messing with the syntax-tables. – Malabarba Nov 13 '14 at 01:52
  • @Malabarba: mostly although it would be nice if movement commands worked as expected in the lispy bits.I had tried messing with font-lock but couldn't get it to work properly: http://git.linaro.org/people/alex.bennee/lava-mode.git/blob/HEAD:/lava-mode.el#l40 – stsquad Nov 13 '14 at 10:55

2 Answers2

7

Ok, let's get some basics straight.

Nesting syntax tables is possible

Syntax tables don't have to be global to the entire buffer. You can apply them as text properties to specific regions. This means you can indeed apply the elisp syntax table only to regions surrounded by backticks.

How do you do that?

Here's one way you can do that. This method does it immediately before font-lock runs through the buffer, so it should specifically prevent your font-locking issues.

(defun endless/set-syntax-then-fontify (beg end loudly)
  "Apply elisp syntax table to relevant regions before calling font-lock."
  (save-match-data
    (save-excursion
      (save-restriction
        (widen)
        (narrow-to-region beg end)
        (while (search-forward "`" nil 'noerror)
          ;; Using `end' here excludes the `, I don't know which syntax you
          ;; want to apply to that.
          (let ((left (match-end 0)))
            (when (search-forward "`" nil 'noerror)
              (add-text-properties
               left (match-beginning 0)
               (list 'syntax-table emacs-lisp-mode-syntax-table))))))))
  (font-lock-default-fontify-region beg end loudly))

In your major-mode definition, you'll need to add:

(set (make-local-variable 'font-lock-fontify-region-function)
     #'endless/set-syntax-then-fontify)

Syntax tables are not the same as syntax highlighting

The syntax highlighter (the font-lock system) uses Syntax Tables as part of its information, so the solution above should prevent the highlighter from going nuts.

However, that is only part of the data, if you also want the text in backticks to be colored exactly as you would see in an elisp buffer, you'll have to extend the function above to do that specifically.

(defun endless/set-syntax-then-fontify (beg end loudly)
  "Apply elisp syntax table to relevant regions before calling font-lock."
  (save-match-data
    (save-excursion
      (save-restriction
        (widen)
        (narrow-to-region beg end)
        (while (search-forward "`" nil 'noerror)
          ;; Using `end' here excludes the `, I don't know which syntax you
          ;; want to apply to that.
          (let ((left (match-end 0)))
            (when (search-forward "`" nil 'noerror)
              (add-text-properties
               left (match-beginning 0)
               (list 'syntax-table emacs-lisp-mode-syntax-table))))))))
  (font-lock-default-fontify-region beg end loudly)
  ;; Do some specific elisp fontifying here
  (save-match-data
    (save-excursion
      (save-restriction
        (widen)
        (narrow-to-region beg end)
        (while (search-forward "`" nil 'noerror)
          (let ((left (match-end 0)))
            (when (search-forward "`" nil 'noerror)
              ;; Call some function to fontify elisp between `left' and (match-beginning 0)
              )))))))
Wilfred Hughes
  • 6,890
  • 2
  • 29
  • 59
Malabarba
  • 22,878
  • 6
  • 78
  • 163
  • can you be a bit more specific? Do I need to nest a call to font lock to do the elisp colouring? – stsquad Nov 17 '14 at 14:46
  • @stsquad I've extended the answer with an example of where you can start for that. I was going to provide a complete solution, but I ran into one obstacle after the other, and eventually reached the conclusion that this would be a whole other question altogether. – Malabarba Nov 17 '14 at 23:51
  • @Malabarba Thanks for the info, but your full solution looks misguided: text properties are part of the buffer text, so you're making the buffer modified every time it's fontified. That'll invalidate several caches and make Emacs consume more CPU for no good reason. You should at least use `with-silent-modifications` (like `font-lock-default-fontify-region` does), but even better would be to move this `add-text-properties` business to `syntax-propertize-function`. – Dmitry Nov 18 '14 at 00:31
6

You don't want to nest one syntax table (which is a vector structure) inside another, you want to set up a buffer where, depending on the position, one syntax table would be used instead of the other.

The other answer describes how to do this using the syntax-table text property. Here's how to do it using one of the "multiple major mode" packages, mmm-mode. It will use the primary mode's everything at the top level of the buffer, and the submode' syntax table, font-lock rules, keymap, etc in the "subregions".

(require 'mmm-auto)
(setq mmm-global-mode 'auto)

(mmm-add-classes
 '((eljson :submode emacs-lisp-mode
           :front ": *\\(`\\)" :back "`"
           :front-match 1
           :face mmm-code-submode-face)))

(mmm-add-mode-ext-class 'json-mode "\\.el\\.json\\'" 'eljson)

This assumes that your mixed-mode files are named *.el.json. Adjust as appropriate.

Now, install mmm-mode, evaluate the above and (only then) open one of the files in question.

Dmitry
  • 3,508
  • 15
  • 20
  • I guess I haven't fully wrapped my head around how the syntax tables work. I assumed they nested as there must be dependant state on the previous characters? My last experience of mmm-mode was nxhtml-mode but I found it rather clunky which is why I switched to using web-mode. – stsquad Nov 13 '14 at 10:57
  • `nxhtml-mode` is rather clunky, yes. `mmm-mode` less so, but it's still complex. I'm not sure if you can do something like this in `web-mode`; ask its author, maybe. – Dmitry Nov 13 '14 at 16:40
  • Syntax tables don't contain any state. Relatedly, `syntax-ppss` cache does. – Dmitry Nov 13 '14 at 16:41
  • sorry I haven't yet accepted the answer. I haven't had time to look at mmm-mode. I'm not sure if this mean I should re-word the question or let it continue to hang? – stsquad Nov 17 '14 at 10:14
  • I don't mind. But if you'd like me to present a more complete answer using mmm-mode, you should include the example snippet of your double markup in the question. – Dmitry Nov 17 '14 at 10:28
  • I've added an except, you can see some more full examples at: http://git.linaro.org/people/alex.bennee/lava-mode.git/blob/HEAD:/examples/generic-hacking-session.json – stsquad Nov 17 '14 at 11:13
  • @stsquad See the edit. – Dmitry Nov 18 '14 at 01:52
  • @stsquad BTW, was putting backquoted expression inside the double quotes intentional, in the example at git.linaro.org? – Dmitry Nov 18 '14 at 13:33
  • yeah, when I wrote the functions I wasn't terribly consistent about making the elisp wrap or not wrap the output in quotes. – stsquad Nov 18 '14 at 13:55
  • Okay. Well, the snippet above can be adapted to either requirement. And if backquotes can occur only as region delimiters, you can just drop `: *` from the front regexp, as well as the `:front-match` property. – Dmitry Nov 18 '14 at 14:15