box-style comments with yasnippet
Asked Answered
C

3

7

I'm looking to write a yasnippet template that would allow me to add a license header to a script buffer in Emacs. Kinda like this, but a bit improved:

  1. The header needs to include per-user data, such as the date name and email of the copyright holder, which can be obtained with embedded elisp expansion from yasnippet.
  2. The header needs to be commented with a syntax depending on the programming mode the file is currently in. There's already a gist of a snippet that does all that. Basically it amounts to embedding (comment-region (point-min) (point)) at the end of your snippet.
  3. Now, I want to change the comment-style to a box. See the emacs documentation for the comment-style variable, or, if you want to see what a box-style comment looks like, just call M-x comment-box on an active region: It calls comment-region with the right options.

A first approach to do that is to setup the style by modifying the end of the previous snippet to:

(let ((comment-style 'box))
            (comment-region (point-min) (point)))

Unfortunately, the indentation gets screwed up, and my box isn't rectangular. If I start from snippet:

Copyright (c) ${1:`(nth 5 (decode-time))`}
All rights reserved.

Redistribution and use in source and binary forms, with or
without modification, are permitted`
      (let ((comment-style 'box))
            (comment-region (point-min) (point)))`

The expansion of that snippet "breaks the box" (I'm debugging this snippet with ocaml comment syntax, not that it should matter):

(**************************************************************)
(* Copyright (c) 2010                                    *)
(* All rights reserved.                                       *)
(*                                                            *)
(* Redistribution and use in source and binary forms, with or *)
(* without modification, are permitted     *)
(**************************************************************)
  • At first I thought the second line was indented based on the size of the pre-expansion embedded code, but in that case, it should make the final *) of that line come 25 spaces too soon, not 4.
  • If it indented based on no text being present at embedding point, the final *) should arrive 4 spaces too late, not too soon.
  • Finally, I don't understand what's going on with the last line, in which there is no embedded code expansion: Usually I don't have any problem getting a square comment box from a paragraph with a short last line (either using comment-box, or the elisp function in the 1st comment-block of this question.

I tried making the comment happen after snippet expansion, to avoid any side-effect, by adding it to yas/after-exit-snippet-hook, replacing the last function of the snippet above with:

(add-hook 'yas/after-exit-snippet-hook
      (let ((comment-style 'box))
            (comment-region (point-min) (point))) nil t)

But that didn't help. Even if it did, it would leave me with an expansion hook that would comment all the snippets I would want to use in that buffer, something I certainly do not want.

I should also add that I tried to set yas/indent-line to fixed, by adding

# expand-env: ((yas/indent-line 'fixed))

at the beginning of my snippet but that didn't change anything. Any ideas on how to get a rectangular box ?


Edit : We have a very nice answer, along with a proposed fix, (kudos & thanks, Seiji !) but a question remains on how to adapt it to the case where one would want to reuse a field, say like the reuse of $1 in:

Copyright (c) ${1:`(nth 5 (decode-time))`}
All rights reserved.

Redistribution and use in $1, in source and binary forms
`(let ((comment-style 'box))
        (comment-region (point-min) (point-at-eol 0)))`

In that case, the template engine copies the (variable-length) value obtained for the field $1, namely 2011, to the last line, at template expansion (after indentation), giving a comment line 2 characters too wide. It becomes hard to predict when writing the template that one should remove 4 characters at this line. Perhaps field reuse and correct indentation are too much to ask for simultaneously. Does any one see a way to do this, though ?

Caracara answered 30/12, 2010 at 15:22 Comment(0)
I
5

Changing your snippet to this one fixes the last line.

Copyright (c) ${1:`(nth 5 (decode-time))`}
All rights reserved.

Redistribution and use in source and binary forms, with or
without modification, are permitted
`(let ((comment-style 'box))(comment-region (point-min) (point-at-eol 0)))`

Note that there is a line break between the license ("... are permitted") and embedded emacs lisp code.

As for the second line, I'll first explain why this occurs and then offer an (ugly) fix. When your snippet is inserted into a file, embedded blocks of lisp are sequentially evaluated from the beginning of buffer to the end. What this means in your snippet is that (nth 5 (decode-time)) has been replaced by 2011 when the next block, (comment-region ...), is evaluated.

As a result, comment-region sees the second line as

Copyright (c) ${1:2011}

So, comment-region puts enough white spaces for this string. Then, the template engine transforms ${1:2011} into 2011, and this transformation shortens the string by 5 characters. This explains the premature appearance of *) by 5 characters in the second line.

One way to fix this situation is putting 5 white spaces back after comment-region is evaluated --- Something along the line of:

Copyright (c) ${1:`(nth 5 (decode-time))`} @
All rights reserved.

Redistribution and use in source and binary forms, with or
without modification, are permitted.
`(let ((comment-style 'box))(comment-region (point-min) (point-at-eol 0)))``
(replace-string "@" "      " nil (point-min) (point))`$0
Interdict answered 3/1, 2011 at 10:0 Comment(2)
Thanks for the point-at-eol suggestion, it completely fixed the issue of the last line. Great explanation, thanks. Your fix works, but it is painful to generalize to many fields on a line (where you have to manually count the boilerplate for each template) and impossible to extend to a reuse of the field $1 elsewhere in the template (where you would have to remove as many whitespaces as the evaluation of the embedded lisp created). I'll wait for a little while, and if no one comes up with a solution for that, I'll "accept" your answer.Caracara
I agree that my solution for the second line is suboptimal. It would be great if yas/after-exit-snipeet-hook works on the text we see. However, it seems the hook works on the underlying text (e.g. ${1:...}). By the way, if you set the hook in # expand-env, the hook only affects the snippet and not other snippets --- by adding # expand-env: ((yas/after-exit-snippet-hook (list (let ((comment-style 'box))(comment-region (point-min) (point-max) nil)))))Interdict
M
1

This might be of some use to you:

# -*- mode: snippet -*-
# name: param
# key:  param
# --
m.parameter :${1:arg},${1:$(make-string (- 14 (string-width text)) ?\ 
                         )}:${2:type}$>

It uses a mirror with a function to create the positioning whitespace. So, for you, this might mean something like this:

(* Copyright (c) ${1:2011}${1:$(make-string (- 72 (string-width text)) ?\ )} *)
(* ${2}${2:$(make-string (- 72 (string-width text)))}*)

Another option might be to use yas/after-exit-snippet-hook, but that seems to be deprecated. (oh, and I meant to filter the text yourself, the comment-region you already tried is cleverer. Try indent-region?)

Monitory answered 3/1, 2011 at 12:24 Comment(1)
This is clever, but unfortunately, as explained in Seiji's answer, the template engine will replace the fields after the execution of blocks, which means that the whitespace inserted by (make-string ... ) is be twice 5 characters short (for ${1:}${1:}). (thanks for the hook suggestion, but I already mentioned my unsuccessful attempts in the original question)Caracara
M
0

This worked for me:

# -*- mode: snippet -*-
# name: box comment block
# key: bbox
# expand-env: ((yas-after-exit-snippet-hook (lambda () (if (buffer-substring yas-snippet-beg yas-snippet-end) (comment-box yas-snippet-beg yas-snippet-end 1)))))
# --
Copyright (c) ${1:`(nth 5 (decode-time))`}
All rights reserved.

Redistribution and use in source and binary forms, with or
without modification, are permitted
$0

I referred to info in this question Run function after specific snippet

Mushro answered 3/2, 2020 at 2:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.