Subject: [bug#64620] [PATCH] gnu: home: Add home-emacs-service-type.

From: Kierin Bell <[email-masked]>
Date: Mon, 28 Aug 2023 18:27:33 -0400

Liliana Marie Prikler <[email-masked]> writes:

> Am Freitag, dem 14.07.2023 um 11:12 -0400 schrieb Kierin Bell
> <[email-masked]>
>> 
>> * gnu/home/services/emacs.scm: New file.
>> * gnu/local.mk (GNU_SYSTEM_MODULES): Add new file.
>> * tests/home/services/emacs.scm: New tests file.
>> * Makefile.am (SCM_TESTS): Add new tests file.
> AFAIK we use "Register" instead of "Add".

Got it; will use "Register" for changes to gnu/local.mk.

>> * doc/guix.texi (Emacs Home Services): New node.
> I'd cut this series into two (or more, see below) patches right around
> here, putting everything below this line into the first patch(es) and
> everything above into the second/nth.
>> * guix/read-print.scm (read-with-comments, read-with-
>> comments/sequence):
>> Add new ELISP? and UNELISP-EXTENSIONS? keyword arguments to support
>> reading Elisp.
>> (%newline-forms): Add `home-emacs-configuration'.
>> (%elisp-special-forms, %elisp-natural-whitespace-string-forms)
>> (%elisp-special-symbol-chars, %elisp-confusable-number-symbols)
>> (%elisp-basic-chars, %elisp-simple-escape-chars): New variables.
>> (special-form-lead, printed-string, symbol->display-string): Add new
>> ELISP? keyword argument.
>> (atom->elisp-string): New helper function.
>> (pretty-print-with-comments): New ELISP? and SPECIAL-FORMS keyword
>> arguments to support serialization to Elisp.  
>
>> General improvements:
>> enable pretty-printing of alists and improper lists; only print lists
>> of
>> constants with one element per line when length exceeds LONG-LIST; do
>> not print newline before special read syntax forms (e.g., `'', `#~',
>> etc.) unless they would exceed MAX-WIDTH; include backslashes when
>> calculating whether a string would exceed MAX-WIDTH; do not print
>> extraneous newline when special form has an empty body; print
>> newlines
>> after list arguments of special forms; print first argument after
>> function on newline with same indentation as function when it would
>> exceed MAX-WIDTH.
>> * tests/read-print.scm: Add new tests and update old tests which fail
>> due to improvements.
> These general improvements should perhaps also been given their own
> patch(es).  Also, since read-print is used in guix style, I'd be
> interested in seeing how the output improves from your changes.  Do you
> have easy comparisons?
>

So when ready, I will open a new issue and send a patch series as per
the manual.

I'll also explore opening a separate issue for the "general
improvements" to (guix read-print) that are not strictly part of the
Elisp serialization functionality.  I'll try to find a way to clearly
annotate that patch with examples of each change and how it affects
output.

Many of the changes I'm calling "general improvements" seem to affect
Elisp output more than Scheme.  E.g., improper lists and alists aren't
used as extensively in Scheme, none of the defined %SPECIAL-FORMS for
Scheme accept list arguments in the right places or empty bodies, etc.

But you make a good point re: guix style.  I managed to contrive an
example package definition that demonstrates most of the changes.

Here is the output of guix style without the patch:

--8<---------------cut here---------------start------------->8---
(define-public foo
  (package
    (name "foo")
    ;; ...
    (arguments
     (list
      ;; *** (1) ***
      #:make-flags #~(list "VERBOSE=1"
                           #~more-flags "..."
                           #~(long-gexp-that-would-protrude-beyond-max-width))
      #:phases #~(modify-phases %standard-phases
                   (add-after 'install 'foo-fix
                     (lambda _
                       (substitute* "some-file"
                         (("match1")
                          ;; *** (2) ***
                          (string-join (list
                                        "list would protrude if not preceded by newline")))
                         (("match2")
                          "replacement")))))))
    ;; *** (3) ***
    (inputs `(,bar ,baz
              ,quux
              ,quuux
              ,quuuux
              ,quuuuux))
    (native-search-paths
     (list (search-path-specification
            (variable "FOO-PATH")
            (files '("foo-dir"))
            ;; *** (4) ***
            (file-pattern
                          "^string\\ with\\ backlashes\\ that\\ would\\ protrude$")
            (file-type 'regular))))
    (properties '((tunable? . #t)
                  ;; *** (5) ***
                  (upstream-name . "foo-with-a-long-upstream-name-that-would-protrude")))
    ;; ...
    (license gpl3+)))
--8<---------------cut here---------------end--------------->8---

...And here it is with the patch:

--8<---------------cut here---------------start------------->8---
(define-public foo
  (package
    (name "foo")
    ;; ...
    (arguments
     (list
      ;; (1) No newline before special read syntaxes when they would not
      ;;     protrude beyond MAX-WIDTH.
      ;;
      ;;     [ Only relevant where a special read syntax occurs after
      ;;     the first argument in a function call and is not preceded
      ;;     by a keyword. ]
      #:make-flags #~(list "VERBOSE=1" #~more-flags "..."
                           #~(long-gexp-that-would-protrude-beyond-max-width))
      #:phases #~(modify-phases %standard-phases
                   (add-after 'install 'foo-fix
                     (lambda _
                       (substitute* "some-file"
                         (("match1")
                          ;; (2) Newline and proper indentation before
                          ;;     first argument of function call when
                          ;;     it would protrude beyond MAX-WIDTH.
                          ;;
                          ;;     [ Only relevant when first argument
                          ;;     of function call is a list that has
                          ;;     elements that would protrude beyond
                          ;;     MAX-WIDTH. ]
                          (string-join
                           (list
                            "list would protrude if not preceded by newline")))
                         ;; XXX: Should there be a newline after
                         ;; `("match2")'?  In Elisp, newlines like
                         ;; that seemed to get annoying, but perhaps
                         ;; it would actually be better here.
                         (("match2") "replacement")))))))
    ;; (3) Quoted lists longer than LONG-LIST with second element on
    ;;     its own line, like the remaining elements.
    ;;
    ;;     [ Fixes an obvious bug. ]
    (inputs `(,bar
              ,baz
              ,quux
              ,quuux
              ,quuuux
              ,quuuuux))
    (native-search-paths
     (list (search-path-specification
            (variable "FOO-PATH")
            (files '("foo-dir"))
            ;; (4) Newline and proper indentation before string with
            ;;     backslashes that would protrude.
            ;;
            ;;    [ Fixes obvious bug --- backslashes must be
            ;;    accounted for in strings to avoid weird issues. ]
            (file-pattern
             "^string\\ with\\ backlashes\\ that\\ would\\ protrude$")
            (file-type 'regular))))
    (properties '((tunable? . #t)
                  ;; (5) Newline before the dot and end of improper lists.
                  (upstream-name
                   . "foo-with-a-long-upstream-name-that-would-protrude")))
    ;; ...
    (license gpl3+)))
--8<---------------cut here---------------end--------------->8---

...Again, these improvements become much more important in a 2k line
Elisp init file.

>> ---
>> 
>> This patch builds on patches from ( and David Wilson for a
>> `home-emacs-service-type' (https://issues.guix.gnu.org/58693,
>> https://issues.guix.gnu.org/60753,
>> https://issues.guix.gnu.org/62549).
>> 
>> Many of the features of the prior patches have been included, but the
>> major focus here is to configure Emacs in Scheme rather than
>> symlinking
>> to existing configuration files.
>> 
>> Here are some of the broad strokes:
>> 
>> * The following record types have been introduced to encapsulate
>>   configuration for Emacs: `emacs-configuration' (for general
>>   configuration), `emacs-package' (for package-specific
>> configuration),
>>   `emacs-keymap' (for configuration of local keymaps), and
>>   `emacs-server' (for configuration of Emacs servers).
>> 
>> * Most configuration fields are either flat lists or alists that are
>>   considerably abstracted from their final serialized Elisp
>>   representation, but escape hatches are provided for both pulling in
>>   existing configuration files and specifying s-expressions directly.
>> 
>> * All serialized Elisp is pretty-printed much how we would expect to
>> see
>>   it in Emacs (for example, with proper indentation according to the
>>   `lisp-indent-function' symbol property, etc.).  This has been
>>   accomplished by adding a new keyword argument to
>>   `pretty-print-with-comments' from `(guix read-print)', among other
>>   improvements.
>> 
>> * Emacs package configuration can either be serialized as `use-
>> package'
>>   forms or as equivalent, more minimalist s-expressions.  Users can
>>   define their own package serializers, too.
>> 
>> * For specifying s-expressions, an "Elisp expression" syntax has been
>>   implemented that is essentially a lighter-weight version G-
>> expressions.
>>   (I try to explain why this is helpful in the documentation.)
>> 
>> * A reader extension has been implemented that allows for "Elisp
>>   expressions" to be specified directly with Elisp read syntax, and
>>   Scheme values (including file-like objects or G-expressions) can in
>>   turn be "unquoted" within that Elisp code.  Also, comments and
>>   whitespace can be included within the Elisp code via the `#;'
>>   (comment), `#>' (newline), and `;^L' (page break) forms.
>> 
>> * Each Emacs server has its own user init and early init files, which
>>   can optionally inherit configuration from the init files used by
>>   non-server Emacsen.  Each server can also inherit the "main"
>>   `user-emacs-directory', or it can use its own subdirectory.
>> 
>> * The `home-emacs-service-type' can be extended, with subordinate
>>   configuration records being merged intelligently when possible.
>> 
>> * A utility function has been provided for generating the
>> aforementioned
>>   Scheme records from an existing Emacs init file:
>>   `elisp-file->home-emacs-configuration'.
>> 
>> Here's an example configuration for the `home-emacs-service-type'
>> demonstrating some of these features:
>> 
>> --8<---------------cut here---------------start------------->8---
>> (use-modules (gnu home)
>>              (gnu services)
>>              (guix gexp)
>>              (gnu home services)
>>              (gnu home services emacs)
>>              (gnu packages emacs-xyz)
>>              (gnu packages file)
>>              (gnu packages compression))
>> 
>> (define %my-function-name 'my--compose-mail)
>> 
>> (define %gnus-init-file
>>   (elisp-file "gnus.el"
>>               (list
>>                (elisp (setq gnus-select-method '(nnnil "")))
>>                (elisp (setq gnus-secondary-select-methods
>>                             '((nnml "")
>>                               (nntp "news.gmane.io"))))
>>                (elisp (setq mail-sources
>>                             '((imap :server "mail.example.net"
>>                                     :user "[email-masked]"
>>                                     :port 993
>>                                     :stream tls))))
>>                ;; Elisp reader extension
>>                #%(define-key global-map [remap compose-mail]
>> #;comment
>>                    '#$%my-function-name nil))))
> I assume that each elisp or #% only handles a single expression, am I
> correct?  Or do we also have (elisp a b) and #%@(a b)?
>

Yes, `elisp' and `#%' are just like `gexp' and `#~'.  `(elisp a b)'
would be a syntax error.  (And #%#$@(a b) is interesting; hadn't tried
that one :) --- but it doesn't work.)

I plan on adding a convenience macro `elisp*' so that (elisp* a b)
expands to (list (elisp a) (elisp b)).  This would make, e.g., the above
`elisp-file' invocation much nicer.

Thanks!

-- 
Kierin Bell
GPG Key: FCF2 5F08 EA4F 2E3D C7C3  0D41 D14A 8CD3 2D97 0B36