diff mbox series

[bug#60753] gnu: home: Add home-emacs-service-type.

Message ID 87v8lby7jq.fsf@daviwil.com
State New
Headers show
Series [bug#60753] gnu: home: Add home-emacs-service-type. | expand

Commit Message

David Wilson Jan. 12, 2023, 5:27 p.m. UTC
Hey (!

"(" <paren@disroot.org> writes:
> home-emacs-shepherd-services isn't defined here :(  Doesn't this cause an unbound-variable
> error?  (Also, most of the reason I wrote this service was to support auto-starting emacs
> --daemon :))

Whoops, in my haste to send out the patch I forgot to take that out!

Yes, I saw that you meant to run Emacs as a daemon and I think it should
be added to this service (or another daemon-specific service) in a
future patch.  I figured it would be easier to get a patch accepted
without the daemon functionality just yet since there seemed to be more
feedback about that aspect in your patch thread.

Here's an update to my patch that actually works (sorry if I'm doing
this wrong, it's been a while since I worked on a patch thread!) --

Co-authored-by: ( <paren@disroot.org>
---
 doc/guix.texi               |  77 +++++++++++++++
 gnu/home/services/emacs.scm | 185 ++++++++++++++++++++++++++++++++++++
 gnu/local.mk                |   1 +
 3 files changed, 263 insertions(+)
 create mode 100644 gnu/home/services/emacs.scm

--
2.38.1

Comments

Ludovic Courtès Jan. 14, 2023, 6 p.m. UTC | #1
Hello!

David Wilson <david@daviwil.com> skribis:

> Yes, I saw that you meant to run Emacs as a daemon and I think it should
> be added to this service (or another daemon-specific service) in a
> future patch.  I figured it would be easier to get a patch accepted
> without the daemon functionality just yet since there seemed to be more
> feedback about that aspect in your patch thread.

Agree, I think we should add the daemon functionality in a subsequent
patch.

Overall it looks nice to me!  Some comments and suggestions:

> +@node Emacs Home Services
> +@subsection Emacs Home Services
> +
> +@defvr {Scheme Variable} home-emacs-service-type

It would be nice if you could start the section with a few sentences
explaining the rationale and what’s being described here.

> +This is the service type for configuring the Emacs text editor.  It
> +enables you to assemble @file{init.el} and @file{early-init.el} files
> +from snippets in your home configuration and other Emacs Lisp files you
> +have in your personal configuration folder.

Maybe like so:

  … to assemble the @file{init.el} (@pxref{Init File,,, emacs, GNU Emacs
  Manual}) and @file{early-init.el} (@pxref{Early Init File,,, emacs,
  GNU Emacs Manual}) files …

> +@example
> +$ cp ~/.emacs.d $XDG_CONFIG_HOME/emacs
> +$ cp ~/.emacs $XDG_CONFIG_HOME/emacs/init.el
> +@end example

I’d drop the prompt.

> +@item @code{init-file} (default: @code{'()})
> +Configuration text or files to include in @file{init.el}.
> +
> +@item @code{early-init-file} (default: @code{'()})
> +Configuration text or files to include in @file{early-init.el}.

What about accepting sexps (or gexps) instead of strings?  As in:

  (init-file '((require 'whatever) (setq something t)))

Also I find it confusing that it’s either text or files.  In the code it
has type ‘text-config’, which means list of file-like objects IIUC, no?

> +@item @code{load-paths} (default: @code{'()})
> +Additional load paths to add to Emacs' @code{load-path} variable.  Lines
> +will be inserted at the beginning of @file{early-init.el}.

Nitpick: I think this should be ‘load-path’ (singular), because it’s one
search path (i.e., a list of directories).

And: s/Additional load paths/Additional directories/

> +@end table
> +@end deftp

Would be nice to have a couple of commented examples here, like you had
in the first message in this thread.  :-)

> +@deftp {Data Type} home-emacs-extension
> +The extension record for @code{home-emacs-service-type}.

Would be nice to have a sentence above, like “This service type can be
extended with @code{home-emacs-extension} records, described below:”.

> +@table @asis
> +@item @code{packages} (default: @code{'()})
> +Additional packages required by the Emacs configuration.
> +
> +@item @code{init-file} (default: @code{'()})
> +Configuration text or files to include in @file{init.el}.
> +
> +@item @code{early-init-file} (default: @code{'()})
> +Configuration text or files to include in @file{early-init.el}.
> +
> +@item @code{load-paths} (default: @code{'()})

Ditto.

> +@end deftp

Would be great to have an example of that too.  :-)

Thank you!

Ludo’.
Andrew Tropin Jan. 15, 2023, 8:02 a.m. UTC | #2
On 2023-01-14 19:00, Ludovic Courtès wrote:

> Hello!
>
> David Wilson <david@daviwil.com> skribis:
>
>> Yes, I saw that you meant to run Emacs as a daemon and I think it should
>> be added to this service (or another daemon-specific service) in a
>> future patch.  I figured it would be easier to get a patch accepted
>> without the daemon functionality just yet since there seemed to be more
>> feedback about that aspect in your patch thread.
>
> Agree, I think we should add the daemon functionality in a subsequent
> patch.
>
> Overall it looks nice to me!  Some comments and suggestions:
>
>> +@node Emacs Home Services
>> +@subsection Emacs Home Services
>> +
>> +@defvr {Scheme Variable} home-emacs-service-type
>
> It would be nice if you could start the section with a few sentences
> explaining the rationale and what’s being described here.
>
>> +This is the service type for configuring the Emacs text editor.  It
>> +enables you to assemble @file{init.el} and @file{early-init.el} files
>> +from snippets in your home configuration and other Emacs Lisp files you
>> +have in your personal configuration folder.
>
> Maybe like so:
>
>   … to assemble the @file{init.el} (@pxref{Init File,,, emacs, GNU Emacs
>   Manual}) and @file{early-init.el} (@pxref{Early Init File,,, emacs,
>   GNU Emacs Manual}) files …
>
>> +@example
>> +$ cp ~/.emacs.d $XDG_CONFIG_HOME/emacs
>> +$ cp ~/.emacs $XDG_CONFIG_HOME/emacs/init.el
>> +@end example
>
> I’d drop the prompt.
>
>> +@item @code{init-file} (default: @code{'()})
>> +Configuration text or files to include in @file{init.el}.
>> +
>> +@item @code{early-init-file} (default: @code{'()})
>> +Configuration text or files to include in @file{early-init.el}.
>
> What about accepting sexps (or gexps) instead of strings?  As in:
>
>   (init-file '((require 'whatever) (setq something t)))

A quick minor note on this approach: it won't be possible to use
#'elisp-function inside such configuration because it will be
interpreted by guile reader, but actually rde lives without this
functionality completely ok.

Do we want something like this possible?

(init-file `((require 'whatever)
             (setq something t)
             (load ,(local-file "old-init.el")))

>
> Also I find it confusing that it’s either text or files.  In the code it
> has type ‘text-config’, which means list of file-like objects IIUC, no?
>
>> +@item @code{load-paths} (default: @code{'()})
>> +Additional load paths to add to Emacs' @code{load-path} variable.  Lines
>> +will be inserted at the beginning of @file{early-init.el}.
>
> Nitpick: I think this should be ‘load-path’ (singular), because it’s one
> search path (i.e., a list of directories).
>
> And: s/Additional load paths/Additional directories/
>
>> +@end table
>> +@end deftp
>
> Would be nice to have a couple of commented examples here, like you had
> in the first message in this thread.  :-)
>
>> +@deftp {Data Type} home-emacs-extension
>> +The extension record for @code{home-emacs-service-type}.
>
> Would be nice to have a sentence above, like “This service type can be
> extended with @code{home-emacs-extension} records, described below:”.
>
>> +@table @asis
>> +@item @code{packages} (default: @code{'()})
>> +Additional packages required by the Emacs configuration.
>> +
>> +@item @code{init-file} (default: @code{'()})
>> +Configuration text or files to include in @file{init.el}.
>> +
>> +@item @code{early-init-file} (default: @code{'()})
>> +Configuration text or files to include in @file{early-init.el}.
>> +
>> +@item @code{load-paths} (default: @code{'()})
>
> Ditto.
>
>> +@end deftp
>
> Would be great to have an example of that too.  :-)
>
> Thank you!
>
> Ludo’.
>
>
>
David Wilson Jan. 16, 2023, 9:25 a.m. UTC | #3
Thanks for the feedback, Ludo!

I'll make the requested changes and send them later today.  I may also
try adding some tests to ensure that the output is what we expect.
There seems to be a `tests/services` folder, should I add a
`tests/home-services` folder to match?

Ludovic Courtès <ludo@gnu.org> writes:

> It would be nice if you could start the section with a few sentences
> explaining the rationale and what’s being described here.

Will do, I'll look at other services to get an idea of the best way to
do this.

> What about accepting sexps (or gexps) instead of strings?  As in:
>
>   (init-file '((require 'whatever) (setq something t)))
>
> Also I find it confusing that it’s either text or files.  In the code it
> has type ‘text-config’, which means list of file-like objects IIUC, no?

Yes, this was a misunderstanding on my part!

Regarding using s-expressions for this, it would certainly look cleaner.
Andrew raises a good concern about it in his response, I'll take a look
at what RDE does for this and see if I can adopt a similar approach.

> Nitpick: I think this should be ‘load-path’ (singular), because it’s one
> search path (i.e., a list of directories).
>
> And: s/Additional load paths/Additional directories/

Will change both of these, thanks!

> Would be nice to have a couple of commented examples here, like you had
> in the first message in this thread.  :-)

Yep, I'll add examples for all the use cases here.

> Would be nice to have a sentence above, like “This service type can be
> extended with @code{home-emacs-extension} records, described below:”.

Will describe and give an example of how to extend the service!

David
Ludovic Courtès Jan. 17, 2023, 9:02 a.m. UTC | #4
Hi,

Andrew Tropin <andrew@trop.in> skribis:

>> What about accepting sexps (or gexps) instead of strings?  As in:
>>
>>   (init-file '((require 'whatever) (setq something t)))
>
> A quick minor note on this approach: it won't be possible to use
> #'elisp-function inside such configuration because it will be
> interpreted by guile reader, but actually rde lives without this
> functionality completely ok.

Specifically:

  (write '#'x)
  |= (syntax x)

But we can use (guix read-print) and ensure that it prints #'.

> Do we want something like this possible?
>
> (init-file `((require 'whatever)
>              (setq something t)
>              (load ,(local-file "old-init.el")))

It’d be nice.  In that case, we’ll want it to be a gexp though:

  #~((require 'whatever) (load #$(local-file …)))

Thanks,
Ludo’.
Andrew Tropin Jan. 17, 2023, 2:46 p.m. UTC | #5
On 2023-01-17 10:02, Ludovic Courtès wrote:

> Hi,
>
> Andrew Tropin <andrew@trop.in> skribis:
>
>>> What about accepting sexps (or gexps) instead of strings?  As in:
>>>
>>>   (init-file '((require 'whatever) (setq something t)))
>>
>> A quick minor note on this approach: it won't be possible to use
>> #'elisp-function inside such configuration because it will be
>> interpreted by guile reader, but actually rde lives without this
>> functionality completely ok.
>
> Specifically:
>
>   (write '#'x)
>   |= (syntax x)
>
> But we can use (guix read-print) and ensure that it prints #'.
>

Do you have any links to docs/sample implementations on the topic of
extending guile reader, so we have an example to start with?  Does guix
workflow language do something like that?

I think it will be cool to hook up a custom reader, ideally comment
preserving, for emacs lisp inside scheme files.

>> Do we want something like this possible?
>>
>> (init-file `((require 'whatever)
>>              (setq something t)
>>              (load ,(local-file "old-init.el")))
>
> It’d be nice.  In that case, we’ll want it to be a gexp though:
>
>   #~((require 'whatever) (load #$(local-file …)))
>

gexps are nice, but do we really need/want them here?  Do you have any
thoughts on what are the benifits over quasiquotes in this case?  Maybe
some examples?
Ludovic Courtès Jan. 23, 2023, 10:18 a.m. UTC | #6
Hi,

Andrew Tropin <andrew@trop.in> skribis:

> On 2023-01-17 10:02, Ludovic Courtès wrote:
>
>> Hi,
>>
>> Andrew Tropin <andrew@trop.in> skribis:
>>
>>>> What about accepting sexps (or gexps) instead of strings?  As in:
>>>>
>>>>   (init-file '((require 'whatever) (setq something t)))
>>>
>>> A quick minor note on this approach: it won't be possible to use
>>> #'elisp-function inside such configuration because it will be
>>> interpreted by guile reader, but actually rde lives without this
>>> functionality completely ok.
>>
>> Specifically:
>>
>>   (write '#'x)
>>   |= (syntax x)
>>
>> But we can use (guix read-print) and ensure that it prints #'.
>>
>
> Do you have any links to docs/sample implementations on the topic of
> extending guile reader, so we have an example to start with?

It’s not the reader but rather the writer that we’d want to tweak.

In (guix read-print), ‘pretty-print-with-comments’ already special cases
quasiquote etc. so that it prints ‘`’ (backtick) and not ‘quasiquote'.
We’d add clauses for ‘syntax’ and ‘quasisyntax’.

> I think it will be cool to hook up a custom reader, ideally comment
> preserving, for emacs lisp inside scheme files.

(guix read-print) is what you want.  :-)

>>> Do we want something like this possible?
>>>
>>> (init-file `((require 'whatever)
>>>              (setq something t)
>>>              (load ,(local-file "old-init.el")))
>>
>> It’d be nice.  In that case, we’ll want it to be a gexp though:
>>
>>   #~((require 'whatever) (load #$(local-file …)))
>>
>
> gexps are nice, but do we really need/want them here?  Do you have any
> thoughts on what are the benifits over quasiquotes in this case?  Maybe
> some examples?

The benefit in the example above is that the gexp would actually work
whereas the sexp wouldn’t :-), unless there’s code somewhere to manually
traverse the sexp adn replace the <local-file> record with its store
item (which is what gexps are about).

I hope that makes sense!

Ludo’.
Andrew Tropin Jan. 26, 2023, 5:06 a.m. UTC | #7
On 2023-01-23 11:18, Ludovic Courtès wrote:

> Hi,
>
> Andrew Tropin <andrew@trop.in> skribis:
>
>> On 2023-01-17 10:02, Ludovic Courtès wrote:
>>
>>> Hi,
>>>
>>> Andrew Tropin <andrew@trop.in> skribis:
>>>
>>>>> What about accepting sexps (or gexps) instead of strings?  As in:
>>>>>
>>>>>   (init-file '((require 'whatever) (setq something t)))
>>>>
>>>> A quick minor note on this approach: it won't be possible to use
>>>> #'elisp-function inside such configuration because it will be
>>>> interpreted by guile reader, but actually rde lives without this
>>>> functionality completely ok.
>>>
>>> Specifically:
>>>
>>>   (write '#'x)
>>>   |= (syntax x)
>>>
>>> But we can use (guix read-print) and ensure that it prints #'.
>>>
>>
>> Do you have any links to docs/sample implementations on the topic of
>> extending guile reader, so we have an example to start with?
>
> It’s not the reader but rather the writer that we’d want to tweak.

Right, it already can read #'x as (syntax x) and we can print it
properly later, but AFAIK comments are ignored by the default reader.
So I would expect to do something (very roughly) like this:

--8<---------------cut here---------------start------------->8---
(parameterize (((@@ (guix gexp) read-procedure) read-with-comments))
  #~(list 'hello ; Comment I would like to preserve during serialization
          'guix))
--8<---------------cut here---------------end--------------->8---

Of course it doesn't work, but I hope demonstrates the idea.

>
> In (guix read-print), ‘pretty-print-with-comments’ already special
> cases quasiquote etc. so that it prints ‘`’ (backtick) and not
> ‘quasiquote'.  We’d add clauses for ‘syntax’ and ‘quasisyntax’.
>

It seems ice-9 pretty-print also preserves backticks, but I see that
pretty-print-with-comments also preserves gexps, which is cool.  Adding
syntax will make it even cooler.

>> I think it will be cool to hook up a custom reader, ideally comment
>> preserving, for emacs lisp inside scheme files.
>
> (guix read-print) is what you want.  :-)
>

Can you give a hint on how to use it for preserving comments, please?

>>>> Do we want something like this possible?
>>>>
>>>> (init-file `((require 'whatever)
>>>>              (setq something t)
>>>>              (load ,(local-file "old-init.el")))
>>>
>>> It’d be nice.  In that case, we’ll want it to be a gexp though:
>>>
>>>   #~((require 'whatever) (load #$(local-file …)))
>>>
>>
>> gexps are nice, but do we really need/want them here?  Do you have any
>> thoughts on what are the benifits over quasiquotes in this case?  Maybe
>> some examples?
>
> The benefit in the example above is that the gexp would actually work
> whereas the sexp wouldn’t :-), unless there’s code somewhere to manually
> traverse the sexp adn replace the <local-file> record with its store
> item (which is what gexps are about).
>
> I hope that makes sense!

With this simple serializer we already achieved quite good results: 
https://git.sr.ht/~abcdw/rde/tree/388d3ad95e8607543df3dcdf26d058b610e77389/src/rde/serializers/lisp.scm#L35

For this input
--8<---------------cut here---------------start------------->8---
`((load ,(local-file "./feature-lists.scm"))
  ,#~(format #f "hello") ; top level gexps are evaluated
  (list ,#~(format #f "hello")) ; nested gexps are not
  ,#~";; hacky comment"
  ;; comment, which is not preserved
  #'hi-fn ; incorrectly serialized, but fixable by alternative
          ; pretty-print
  )
--8<---------------cut here---------------end--------------->8---

it provides quite satisfying results:
--8<---------------cut here---------------start------------->8---
(load "/gnu/store/xb6ma0mcgg1zzq645s63arvy3qskmbiz-feature-lists.scm")
hello
(list (format #f "hello"))
;; hacky comment
(syntax hi-fn)
--8<---------------cut here---------------end--------------->8---

It's a little incosistent (top level gexp are evaluated, but nested are
not), comments are not preserved and #' serialized incorrectly, but
other than that it works very good.

WDYT about overall approach used here?  or we can do it radically
better?
Reily Siegel Jan. 26, 2023, 6:50 p.m. UTC | #8
Ludovic Courtès <ludo@gnu.org> writes:

> Specifically:
>
>   (write '#'x)
>   |= (syntax x)
>
> But we can use (guix read-print) and ensure that it prints #'.

The way I get around this in my config is adding

#+begin_source emacs-lisp
;; #' exports a scheme (syntax ...) form. Treat this as a
;; (function ...) form.
(defalias 'syntax 'function)
#+end_source

This is a very hacky solution, but if you wanted to not modify the
reader, you could add this to the beginning of init.el with the service.

--
Reily Siegel
Ludovic Courtès Jan. 31, 2023, 4:26 p.m. UTC | #9
Hi Andrew,

Andrew Tropin <andrew@trop.in> skribis:

>>> I think it will be cool to hook up a custom reader, ideally comment
>>> preserving, for emacs lisp inside scheme files.
>>
>> (guix read-print) is what you want.  :-)
>>
>
> Can you give a hint on how to use it for preserving comments, please?

It can be used like this:

--8<---------------cut here---------------start------------->8---
scheme@(guile-user)> ,use(guix read-print)
scheme@(guile-user)> (pretty-print-with-comments (current-output-port) `(list foo ,(comment ";ooh!\n" #t) bar))
(list foo ;ooh!
      bar)$5 = 10
scheme@(guile-user)> (call-with-input-string "(list foo ;oh!\nbar)" read-with-comments)
$6 = (list foo #<<comment> str: ";oh!\n" margin?: #t> bar)
--8<---------------cut here---------------end--------------->8---

There’s a <comment> record type.

Now let’s see perhaps what we need to get ‘home-emacs-service-type’
merged, and what we can keep as future work.  Thoughts?

Ludo’.
Jelle Licht Feb. 1, 2023, 12:59 p.m. UTC | #10
Andrew Tropin <andrew@trop.in> writes:

> On 2023-01-23 11:18, Ludovic Courtès wrote:
>
>> Hi,
>>
>> Andrew Tropin <andrew@trop.in> skribis:
>>
>>> On 2023-01-17 10:02, Ludovic Courtès wrote:
>>>
>>>> Hi,
>>>>
>>>> Andrew Tropin <andrew@trop.in> skribis:
>>>>
>>>>>> What about accepting sexps (or gexps) instead of strings?  As in:
>>>>>>
>>>>>>   (init-file '((require 'whatever) (setq something t)))
>>>>>
>>>>> A quick minor note on this approach: it won't be possible to use
>>>>> #'elisp-function inside such configuration because it will be
>>>>> interpreted by guile reader, but actually rde lives without this
>>>>> functionality completely ok.
>>>>
>>>> Specifically:
>>>>
>>>>   (write '#'x)
>>>>   |= (syntax x)
>>>>
>>>> But we can use (guix read-print) and ensure that it prints #'.
>>>>
>>>
>>> Do you have any links to docs/sample implementations on the topic of
>>> extending guile reader, so we have an example to start with?
>>
>> It’s not the reader but rather the writer that we’d want to tweak.
>
> Right, it already can read #'x as (syntax x) and we can print it
> properly later, but AFAIK comments are ignored by the default reader.
> So I would expect to do something (very roughly) like this:
>
> --8<---------------cut here---------------start------------->8---
> (parameterize (((@@ (guix gexp) read-procedure) read-with-comments))
>   #~(list 'hello ; Comment I would like to preserve during serialization
>           'guix))
> --8<---------------cut here---------------end--------------->8---
>
> Of course it doesn't work, but I hope demonstrates the idea.
>
>>
>> In (guix read-print), ‘pretty-print-with-comments’ already special
>> cases quasiquote etc. so that it prints ‘`’ (backtick) and not
>> ‘quasiquote'.  We’d add clauses for ‘syntax’ and ‘quasisyntax’.
>>
>
> It seems ice-9 pretty-print also preserves backticks, but I see that
> pretty-print-with-comments also preserves gexps, which is cool.  Adding
> syntax will make it even cooler.
>
>>> I think it will be cool to hook up a custom reader, ideally comment
>>> preserving, for emacs lisp inside scheme files.
>>
>> (guix read-print) is what you want.  :-)
>>
>
> Can you give a hint on how to use it for preserving comments, please?
>
>>>>> Do we want something like this possible?
>>>>>
>>>>> (init-file `((require 'whatever)
>>>>>              (setq something t)
>>>>>              (load ,(local-file "old-init.el")))
>>>>
>>>> It’d be nice.  In that case, we’ll want it to be a gexp though:
>>>>
>>>>   #~((require 'whatever) (load #$(local-file …)))
>>>>
>>>
>>> gexps are nice, but do we really need/want them here?  Do you have any
>>> thoughts on what are the benifits over quasiquotes in this case?  Maybe
>>> some examples?
>>
>> The benefit in the example above is that the gexp would actually work
>> whereas the sexp wouldn’t :-), unless there’s code somewhere to manually
>> traverse the sexp adn replace the <local-file> record with its store
>> item (which is what gexps are about).
>>
>> I hope that makes sense!
>
> With this simple serializer we already achieved quite good results: 
> https://git.sr.ht/~abcdw/rde/tree/388d3ad95e8607543df3dcdf26d058b610e77389/src/rde/serializers/lisp.scm#L35
>
> For this input
> --8<---------------cut here---------------start------------->8---
> `((load ,(local-file "./feature-lists.scm"))
>   ,#~(format #f "hello") ; top level gexps are evaluated
>   (list ,#~(format #f "hello")) ; nested gexps are not
>   ,#~";; hacky comment"
>   ;; comment, which is not preserved
>   #'hi-fn ; incorrectly serialized, but fixable by alternative
>           ; pretty-print
>   )
> --8<---------------cut here---------------end--------------->8---
>
> it provides quite satisfying results:
> --8<---------------cut here---------------start------------->8---
> (load "/gnu/store/xb6ma0mcgg1zzq645s63arvy3qskmbiz-feature-lists.scm")
> hello
> (list (format #f "hello"))
> ;; hacky comment
> (syntax hi-fn)
> --8<---------------cut here---------------end--------------->8---
>
> It's a little incosistent (top level gexp are evaluated, but nested are
> not), comments are not preserved and #' serialized incorrectly, but
> other than that it works very good.
>
> WDYT about overall approach used here?  or we can do it radically
> better?

Not saying it's better in any particular way, but I have had this locally
for all my elisp-read-by-guile-written-back-to-elisp needs:

--8<---------------cut here---------------start------------->8---
(define-module (jlicht build elisp-write)
  #:use-module (ice-9 match)
  #:use-module (srfi srfi-1)
  #:export (elisp-write))

(define (elisp-write in-list? exp port)
  "Stack-blowing implementation that writes guile's internal elisp
representation to something that can be parsed by Emacs."
  ;; Definitions from (language elisp parser)'s quotation-symbols: 
  (define symbol-strings
    '((#{`}# . "`")
      (#{,}# . ",")
      (#{,@}# . ",@")))
  (define (elisp-symbol? sym)
    (assq sym symbol-strings))
  (define (write-elisp-symbol sym port)
    (format port "~A" (assq-ref symbol-strings sym)))
  
  (match exp
    (((? elisp-symbol? sym) rest)
     (write-elisp-symbol sym port)
     (elisp-write in-list? rest port))
    ;; Vector expression
    (#(vs ...)
     (format port "[")
     (elisp-write #t vs port)
     (format port "]"))
    ;; Guile elisp implementation detail
    ('(%set-lexical-binding-mode #f) 'skip)
    ;; List walker
    ((e ...)
     (when (not in-list?) (format port "("))
     (unless (null? e)
       (elisp-write #f (car e) port)
       (for-each (lambda (v)
                   (format port " ")
                   (elisp-write #f v port)) (cdr e)))
     (when (not in-list?) (format port ")")))
    ;; dotted pair
    ((and (? pair?) (? dotted-list? l))
     (format port "(")
     (elisp-write #t (drop-right l 0) port)
     (format port " . ")
     (elisp-write #t (take-right l 0) port)
     (format port ")"))
    ;; Print simple primitives
    (_ (write exp port))))
--8<---------------cut here---------------end--------------->8---

On the reader side I just use guile's elisp reader:

--8<---------------cut here---------------start------------->8---
(define-module (jlicht test elisp)
  #:use-module (language elisp parser)
  #:use-module (jlicht build elisp-write)
  #:use-module (srfi srfi-26)
  #:use-module (srfi srfi-64))

(eval-when (expand load eval)
  (read-hash-extend #\e (lambda (chr port) (read-elisp port))))

(set! test-log-to-file #f)

(define (roundtrip expr)
  (let ((written (call-with-output-string (cut elisp-write #f expr <>))))
    (call-with-input-string written read-elisp)))

(define-syntax test-roundtrip-equals
  (syntax-rules ()
    ((_ expr)
     (let ((e1 (roundtrip expr)))
       (test-equal e1 (roundtrip e1))))))

(define runner (test-runner-simple))

(test-with-runner runner
  (test-begin "roundtrip-elisp-fixed-point")
  (test-roundtrip-equals 12)
  (test-roundtrip-equals "hello")
  (test-roundtrip-equals '#e#'my-fn)
  (test-roundtrip-equals '#e[a b c])
  (test-roundtrip-equals '#e`(+ 1 2 ,@(a b) ,c))
  (test-end "roundtrip-elisp-fixed-point"))

(exit (test-runner-fail-count runner))
--8<---------------cut here---------------end--------------->8---

I've also hooked it up in combination with a sequence of calls to
`scheme-file' -> `computed-file' called `elisp-file', but that's a bit
more hacky and less relevant to the current discussion.

- Jelle
Andrew Tropin Feb. 1, 2023, 1:46 p.m. UTC | #11
On 2023-02-01 12:59, Jelle Licht wrote:

> Andrew Tropin <andrew@trop.in> writes:
>
>> On 2023-01-23 11:18, Ludovic Courtès wrote:
>>
>>> Hi,
>>>
>>> Andrew Tropin <andrew@trop.in> skribis:
>>>
>>>> On 2023-01-17 10:02, Ludovic Courtès wrote:
>>>>
>>>>> Hi,
>>>>>
>>>>> Andrew Tropin <andrew@trop.in> skribis:
>>>>>
>>>>>>> What about accepting sexps (or gexps) instead of strings?  As in:
>>>>>>>
>>>>>>>   (init-file '((require 'whatever) (setq something t)))
>>>>>>
>>>>>> A quick minor note on this approach: it won't be possible to use
>>>>>> #'elisp-function inside such configuration because it will be
>>>>>> interpreted by guile reader, but actually rde lives without this
>>>>>> functionality completely ok.
>>>>>
>>>>> Specifically:
>>>>>
>>>>>   (write '#'x)
>>>>>   |= (syntax x)
>>>>>
>>>>> But we can use (guix read-print) and ensure that it prints #'.
>>>>>
>>>>
>>>> Do you have any links to docs/sample implementations on the topic of
>>>> extending guile reader, so we have an example to start with?
>>>
>>> It’s not the reader but rather the writer that we’d want to tweak.
>>
>> Right, it already can read #'x as (syntax x) and we can print it
>> properly later, but AFAIK comments are ignored by the default reader.
>> So I would expect to do something (very roughly) like this:
>>
>> --8<---------------cut here---------------start------------->8---
>> (parameterize (((@@ (guix gexp) read-procedure) read-with-comments))
>>   #~(list 'hello ; Comment I would like to preserve during serialization
>>           'guix))
>> --8<---------------cut here---------------end--------------->8---
>>
>> Of course it doesn't work, but I hope demonstrates the idea.
>>
>>>
>>> In (guix read-print), ‘pretty-print-with-comments’ already special
>>> cases quasiquote etc. so that it prints ‘`’ (backtick) and not
>>> ‘quasiquote'.  We’d add clauses for ‘syntax’ and ‘quasisyntax’.
>>>
>>
>> It seems ice-9 pretty-print also preserves backticks, but I see that
>> pretty-print-with-comments also preserves gexps, which is cool.  Adding
>> syntax will make it even cooler.
>>
>>>> I think it will be cool to hook up a custom reader, ideally comment
>>>> preserving, for emacs lisp inside scheme files.
>>>
>>> (guix read-print) is what you want.  :-)
>>>
>>
>> Can you give a hint on how to use it for preserving comments, please?
>>
>>>>>> Do we want something like this possible?
>>>>>>
>>>>>> (init-file `((require 'whatever)
>>>>>>              (setq something t)
>>>>>>              (load ,(local-file "old-init.el")))
>>>>>
>>>>> It’d be nice.  In that case, we’ll want it to be a gexp though:
>>>>>
>>>>>   #~((require 'whatever) (load #$(local-file …)))
>>>>>
>>>>
>>>> gexps are nice, but do we really need/want them here?  Do you have any
>>>> thoughts on what are the benifits over quasiquotes in this case?  Maybe
>>>> some examples?
>>>
>>> The benefit in the example above is that the gexp would actually work
>>> whereas the sexp wouldn’t :-), unless there’s code somewhere to manually
>>> traverse the sexp adn replace the <local-file> record with its store
>>> item (which is what gexps are about).
>>>
>>> I hope that makes sense!
>>
>> With this simple serializer we already achieved quite good results: 
>> https://git.sr.ht/~abcdw/rde/tree/388d3ad95e8607543df3dcdf26d058b610e77389/src/rde/serializers/lisp.scm#L35
>>
>> For this input
>> --8<---------------cut here---------------start------------->8---
>> `((load ,(local-file "./feature-lists.scm"))
>>   ,#~(format #f "hello") ; top level gexps are evaluated
>>   (list ,#~(format #f "hello")) ; nested gexps are not
>>   ,#~";; hacky comment"
>>   ;; comment, which is not preserved
>>   #'hi-fn ; incorrectly serialized, but fixable by alternative
>>           ; pretty-print
>>   )
>> --8<---------------cut here---------------end--------------->8---
>>
>> it provides quite satisfying results:
>> --8<---------------cut here---------------start------------->8---
>> (load "/gnu/store/xb6ma0mcgg1zzq645s63arvy3qskmbiz-feature-lists.scm")
>> hello
>> (list (format #f "hello"))
>> ;; hacky comment
>> (syntax hi-fn)
>> --8<---------------cut here---------------end--------------->8---
>>
>> It's a little incosistent (top level gexp are evaluated, but nested are
>> not), comments are not preserved and #' serialized incorrectly, but
>> other than that it works very good.
>>
>> WDYT about overall approach used here?  or we can do it radically
>> better?
>
> Not saying it's better in any particular way, but I have had this locally
> for all my elisp-read-by-guile-written-back-to-elisp needs:

I saw it in guix-home-manager and probably you've made the thread on
rde-devel too.  I tried some parts of this code, but didn't succeed to
get a complete working out of it, now I have a little more knowledge and
hope will get better results :)

>
> --8<---------------cut here---------------start------------->8---
> (define-module (jlicht build elisp-write)
>   #:use-module (ice-9 match)
>   #:use-module (srfi srfi-1)
>   #:export (elisp-write))
>
> (define (elisp-write in-list? exp port)
>   "Stack-blowing implementation that writes guile's internal elisp
> representation to something that can be parsed by Emacs."
>   ;; Definitions from (language elisp parser)'s quotation-symbols: 
>   (define symbol-strings
>     '((#{`}# . "`")
>       (#{,}# . ",")
>       (#{,@}# . ",@")))
>   (define (elisp-symbol? sym)
>     (assq sym symbol-strings))
>   (define (write-elisp-symbol sym port)
>     (format port "~A" (assq-ref symbol-strings sym)))
>   
>   (match exp
>     (((? elisp-symbol? sym) rest)
>      (write-elisp-symbol sym port)
>      (elisp-write in-list? rest port))
>     ;; Vector expression
>     (#(vs ...)
>      (format port "[")
>      (elisp-write #t vs port)
>      (format port "]"))
>     ;; Guile elisp implementation detail
>     ('(%set-lexical-binding-mode #f) 'skip)
>     ;; List walker
>     ((e ...)
>      (when (not in-list?) (format port "("))
>      (unless (null? e)
>        (elisp-write #f (car e) port)
>        (for-each (lambda (v)
>                    (format port " ")
>                    (elisp-write #f v port)) (cdr e)))
>      (when (not in-list?) (format port ")")))
>     ;; dotted pair
>     ((and (? pair?) (? dotted-list? l))
>      (format port "(")
>      (elisp-write #t (drop-right l 0) port)
>      (format port " . ")
>      (elisp-write #t (take-right l 0) port)
>      (format port ")"))
>     ;; Print simple primitives
>     (_ (write exp port))))
> --8<---------------cut here---------------end--------------->8---
>
> On the reader side I just use guile's elisp reader:
>
> --8<---------------cut here---------------start------------->8---
> (define-module (jlicht test elisp)
>   #:use-module (language elisp parser)
>   #:use-module (jlicht build elisp-write)
>   #:use-module (srfi srfi-26)
>   #:use-module (srfi srfi-64))
>
> (eval-when (expand load eval)
>   (read-hash-extend #\e (lambda (chr port) (read-elisp port))))

That's what I was looking for.  If you have any links related to the
topic of the reader extension, please let me know.

>
> (set! test-log-to-file #f)
>
> (define (roundtrip expr)
>   (let ((written (call-with-output-string (cut elisp-write #f expr <>))))
>     (call-with-input-string written read-elisp)))
>
> (define-syntax test-roundtrip-equals
>   (syntax-rules ()
>     ((_ expr)
>      (let ((e1 (roundtrip expr)))
>        (test-equal e1 (roundtrip e1))))))
>
> (define runner (test-runner-simple))
>
> (test-with-runner runner
>   (test-begin "roundtrip-elisp-fixed-point")
>   (test-roundtrip-equals 12)
>   (test-roundtrip-equals "hello")
>   (test-roundtrip-equals '#e#'my-fn)
>   (test-roundtrip-equals '#e[a b c])
>   (test-roundtrip-equals '#e`(+ 1 2 ,@(a b) ,c))

It would be cool to make elisp-unquote for #e, but I think I can take a
look at ungexp to understand how to implement it.

>   (test-end "roundtrip-elisp-fixed-point"))
>
> (exit (test-runner-fail-count runner))
> --8<---------------cut here---------------end--------------->8---
>
> I've also hooked it up in combination with a sequence of calls to
> `scheme-file' -> `computed-file' called `elisp-file', but that's a bit
> more hacky and less relevant to the current discussion.
>
> - Jelle

Thank you very much!
Andrew Tropin Feb. 1, 2023, 2:06 p.m. UTC | #12
On 2023-01-31 17:26, Ludovic Courtès wrote:

> Hi Andrew,
>
> Andrew Tropin <andrew@trop.in> skribis:
>
>>>> I think it will be cool to hook up a custom reader, ideally comment
>>>> preserving, for emacs lisp inside scheme files.
>>>
>>> (guix read-print) is what you want.  :-)
>>>
>>
>> Can you give a hint on how to use it for preserving comments, please?
>
> It can be used like this:
>
> --8<---------------cut here---------------start------------->8---
> scheme@(guile-user)> ,use(guix read-print)
> scheme@(guile-user)> (pretty-print-with-comments (current-output-port) `(list foo ,(comment ";ooh!\n" #t) bar))
> (list foo ;ooh!
>       bar)$5 = 10
> scheme@(guile-user)> (call-with-input-string "(list foo ;oh!\nbar)" read-with-comments)
> $6 = (list foo #<<comment> str: ";oh!\n" margin?: #t> bar)
> --8<---------------cut here---------------end--------------->8---
>
> There’s a <comment> record type.

Yep, I already experimented with it, but it's not exactly what I'm
looking for, Jelle gave a few ideas and code snippets which looks closer
to what I searching.  Pretty printer and read function are useful, but
not directly related to my question about reader.

>
> Now let’s see perhaps what we need to get ‘home-emacs-service-type’
> merged, and what we can keep as future work.  Thoughts?

We have not perfect, but working quite good implementation of lisp
serializer in rde, which we use in rde's home-emacs-service-type:
https://git.sr.ht/~abcdw/rde/tree/d0ea604282c9aeb0b121f51979373b4aa40f9bcb/item/tests/rde/serializers/lisp-test.scm
https://git.sr.ht/~abcdw/rde/tree/d0ea604282c9aeb0b121f51979373b4aa40f9bcb/item/src/rde/home/services/emacs.scm#L93

But I'm not completely satisfyied with it and don't want to upstream it
in current state, I plan to experiment with Jelle's code and get a
feeling of how his approach works, also want to dive into reader
extensions topic.  If I get satisfying solution I will share it.  It can
take quite a while, so don't expect it to happen anytime soon; it can,
but unlikely :) If anyone want to work in parallel and propose
alternative implementation, I would be glad to give a feedback on it.
David Wilson Feb. 10, 2023, 7:50 a.m. UTC | #13
Ludovic Courtès <ludo@gnu.org> writes:

> Now let’s see perhaps what we need to get ‘home-emacs-service-type’
> merged, and what we can keep as future work.  Thoughts?

Sorry for the long delay in getting back to this!  To summarize what
we've discussed so far:

- It's possibly best to use g-expressions for the init.el file contents
  because it gives ultimate flexibility on how one can describe their
  Emacs configuration in Scheme while pulling in files from their local
  configuration folder.  Are we concerned whether this may be harder for
  users to understand and adopt?

- We'll postpone any work to ensure that comments and #'function
  references can be properly represented so that we can get the basic
  `home-emacs-service' merged for now.  Future patches will improve on
  that situation.

Is that accurate?  Either way, I'll spend some time today responding to
the feedback so that we can get this merged.

Thanks!

David
Ludovic Courtès Feb. 20, 2023, 11:10 a.m. UTC | #14
Hi David,

David Wilson <david@daviwil.com> skribis:

> Ludovic Courtès <ludo@gnu.org> writes:
>
>> Now let’s see perhaps what we need to get ‘home-emacs-service-type’
>> merged, and what we can keep as future work.  Thoughts?
>
> Sorry for the long delay in getting back to this!  To summarize what
> we've discussed so far:
>
> - It's possibly best to use g-expressions for the init.el file contents
>   because it gives ultimate flexibility on how one can describe their
>   Emacs configuration in Scheme while pulling in files from their local
>   configuration folder.  Are we concerned whether this may be harder for
>   users to understand and adopt?

Ease of use should always be a concern IMO.  I’d expect that writing
gexps will feel more convenient than in sexps-in-Scheme-strings for the
target Elisp audience.

> - We'll postpone any work to ensure that comments and #'function
>   references can be properly represented so that we can get the basic
>   `home-emacs-service' merged for now.  Future patches will improve on
>   that situation.
>
> Is that accurate?  Either way, I'll spend some time today responding to
> the feedback so that we can get this merged.

Sounds good to me!

Ludo’.
diff mbox series

Patch

diff --git a/doc/guix.texi b/doc/guix.texi
index 751d0957d8..62fefde1ea 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -111,6 +111,7 @@  Copyright @copyright{} 2022 (@*
 Copyright @copyright{} 2022 John Kehayias@*
 Copyright @copyright{} 2022 Ivan Vilata-i-Balaguer@*
 Copyright @copyright{} 2023 Giacomo Leidi@*
+Copyright @copyright{} 2023 David Wilson@*

 Permission is granted to copy, distribute and/or modify this document
 under the terms of the GNU Free Documentation License, Version 1.3 or
@@ -41061,6 +41062,7 @@  services)}.
 * Shepherd: Shepherd Home Service.                     Managing User's Daemons.
 * SSH: Secure Shell.                                   Setting up the secure shell client.
 * Desktop: Desktop Home Services.                      Services for graphical environments.
+* Emacs: Emacs Home Services.                          Services for configuring Emacs.
 * Guix: Guix Home Services.                            Services for Guix.
 @end menu
 @c In addition to that Home Services can provide
@@ -41914,6 +41916,81 @@  The package providing the @code{/bin/dbus-daemon} command.
 @end table
 @end deftp

+@node Emacs Home Services
+@subsection Emacs Home Services
+
+@defvr {Scheme Variable} home-emacs-service-type
+This is the service type for configuring the Emacs text editor.  It
+enables you to assemble @file{init.el} and @file{early-init.el} files
+from snippets in your home configuration and other Emacs Lisp files you
+have in your personal configuration folder.
+
+This service can be extended using the @code{home-emacs-extension} type.
+
+Note that if you have an existing @file{~/.emacs} and/or
+@file{~/.emacs.d}, the configuration aspect of this service will not be
+loaded, as the former location takes precedence over
+@file{~/.config/emacs}.  This service uses the latter path in the
+interest of cleanliness.  To migrate to the XDG directory, run these
+commands:
+
+@example
+$ cp ~/.emacs.d $XDG_CONFIG_HOME/emacs
+$ cp ~/.emacs $XDG_CONFIG_HOME/emacs/init.el
+@end example
+@end defvr
+
+@deftp {Data Type} home-emacs-configuration
+The configuration record for @code{home-emacs-service-type}.
+
+@table @asis
+@item @code{emacs} (default: @code{emacs})
+The package providing the @file{/bin/emacs} command.
+
+@item @code{packages} (default: @code{'()})
+Additional packages required by the Emacs configuration.
+
+@item @code{user-emacs-directory} (default: @file{~/.cache/emacs})
+The directory beneath which additional per-user Emacs-specific files are placed.
+
+@item @code{init-file} (default: @code{'()})
+Configuration text or files to include in @file{init.el}.
+
+@item @code{early-init-file} (default: @code{'()})
+Configuration text or files to include in @file{early-init.el}.
+
+@item @code{load-paths} (default: @code{'()})
+Additional load paths to add to Emacs' @code{load-path} variable.  Lines
+will be inserted at the beginning of @file{early-init.el}.
+
+@item @code{native-compile?} (default: @code{#f})
+Whether to compile all @code{packages}, using the provided @code{emacs}
+package in place of @code{emacs-minimal}, which will enable native
+compilation if the @code{emacs} package supports it.  All
+non-@code{-minimal} Emacs packages at version 28 or above should support
+native compilation.
+@end table
+@end deftp
+
+@deftp {Data Type} home-emacs-extension
+The extension record for @code{home-emacs-service-type}.
+
+@table @asis
+@item @code{packages} (default: @code{'()})
+Additional packages required by the Emacs configuration.
+
+@item @code{init-file} (default: @code{'()})
+Configuration text or files to include in @file{init.el}.
+
+@item @code{early-init-file} (default: @code{'()})
+Configuration text or files to include in @file{early-init.el}.
+
+@item @code{load-paths} (default: @code{'()})
+Additional load paths to add to Emacs' @code{load-path} variable.  Lines
+will be inserted at the beginning of @file{early-init.el}.
+@end table
+@end deftp
+
 @node Guix Home Services
 @subsection Guix Home Services

diff --git a/gnu/home/services/emacs.scm b/gnu/home/services/emacs.scm
new file mode 100644
index 0000000000..7a821fed8a
--- /dev/null
+++ b/gnu/home/services/emacs.scm
@@ -0,0 +1,185 @@ 
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2022 ( <paren@disroot.org>
+;;; Copyright © 2023 David Wilson <david@daviwil.com>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (gnu home services emacs)
+  #:use-module (gnu home services)
+  #:autoload   (gnu packages emacs) (emacs-minimal
+                                     emacs)
+  #:use-module (gnu services configuration)
+  #:use-module (guix gexp)
+  #:use-module (guix packages)
+  #:use-module (guix records)
+  #:use-module (ice-9 match)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-26)
+
+  #:export (emacs-variables
+            home-emacs-configuration
+            home-emacs-extension
+            home-emacs-service-type))
+
+(define list-of-file-likes?
+  (list-of file-like?))
+
+(define (string-or-file-like? val)
+  (or (string? val)
+      (file-like? val)))
+
+(define list-of-string-or-file-likes?
+  (list-of string-or-file-like?))
+
+(define-configuration/no-serialization home-emacs-configuration
+  (emacs
+   (file-like emacs)
+   "The package providing the @file{/bin/emacs} command.")
+  (packages
+   (list-of-file-likes '())
+   "Additional packages required by the Emacs configuration.")
+  (user-emacs-directory
+   (string "~/.cache/emacs")
+   "Directory beneath which additional per-user Emacs-specific files are placed.")
+  (init-file
+   (text-config '())
+   "Configuration text or files to include in init.el.")
+  (early-init-file
+   (text-config '())
+   "Configuration text or files to include in early-init.el.")
+  (load-paths
+   (list-of-string-or-file-likes '())
+   "Additional load paths to add to Emacs' @code{load-path} variable.  Lines
+will be inserted at the beginning of early-init.el.")
+  (native-compile?
+   (boolean #f)
+   "Whether to compile the @code{packages} using the Emacs package
+provided as the value of the @code{emacs} field, which will enable
+native compilation if the @code{emacs} package supports it."))
+
+(define (home-emacs-profile-packages config)
+  (cons (home-emacs-configuration-emacs config)
+        (home-emacs-configuration-packages config)))
+
+(define (home-emacs-transformed-packages config)
+  (map (if (home-emacs-configuration-native-compile? config)
+           (package-input-rewriting
+            `((,emacs-minimal
+              . ,(home-emacs-configuration-emacs config))))
+           identity)
+       (let ((packages (home-emacs-configuration-packages config)))
+         (concatenate
+          (cons packages
+                (map (compose (cute map second <>)
+                              package-transitive-propagated-inputs)
+                     packages))))))
+
+(define (serialize-emacs-load-paths config)
+  #~(string-append
+     ";; Additional load paths\n"
+     #$@(map (lambda (load-path)
+               #~(format #f "(add-to-list 'load-path \"~a\")" #$load-path))
+             (home-emacs-configuration-load-paths config))
+     "\n\n"))
+
+(define (serialize-emacs-user-directory config)
+  (format #f
+          ";; Set the `user-emacs-directory` to a writeable path\n(setq user-emacs-directory \"~a\")\n\n"
+          (home-emacs-configuration-user-emacs-directory config)))
+
+(define (home-emacs-xdg-configuration-files config)
+  `(("emacs/early-init.el"
+     ,(apply mixed-text-file
+             (cons* "early-init.el"
+                    (serialize-emacs-load-paths config)
+                    (serialize-emacs-user-directory config)
+                    (home-emacs-configuration-early-init-file config))))
+    ("emacs/init.el"
+     ,(apply mixed-text-file
+             (cons "init.el"
+                   (home-emacs-configuration-init-file config))))))
+
+(define-configuration/no-serialization home-emacs-extension
+  (packages
+   (list-of-file-likes '())
+   "Additional packages required by the Emacs configuration.")
+  (init-file
+   (text-config '())
+   "Configuration text or files to include in init.el.")
+  (early-init-file
+   (text-config '())
+   "Configuration text or files to include in early-init.el.")
+  (load-paths
+   (list-of-string-or-file-likes '())
+   "Additional load paths to add to Emacs' @code{load-path} variable.  Lines
+will be inserted at the beginning of early-init.el."))
+
+(define (home-emacs-extensions original-config extension-configs)
+  (match-record original-config <home-emacs-configuration>
+    (packages load-paths init-file early-init-file)
+    (home-emacs-configuration
+     (inherit original-config)
+     (packages
+      (append packages
+              (append-map
+               home-emacs-extension-packages extension-configs)))
+     (init-file
+      (append init-file
+              (append-map
+               home-emacs-extension-init-file extension-configs)))
+     (early-init-file
+      (append early-init-file
+              (append-map
+               home-emacs-extension-early-init-file extension-configs)))
+     (load-paths
+      (append load-paths
+              (append-map
+               home-emacs-extension-load-paths extension-configs))))))
+
+(define home-emacs-service-type
+  (service-type
+   (name 'home-emacs)
+   (extensions
+    (list (service-extension
+           home-profile-service-type
+           home-emacs-profile-packages)
+          (service-extension
+           home-xdg-configuration-files-service-type
+           home-emacs-xdg-configuration-files)))
+   (default-value (home-emacs-configuration))
+   (compose identity)
+   (extend home-emacs-extensions)
+   (description
+    "Configure the GNU Emacs extensible text editor.")))
+
+(define scheme-value->emacs-value
+  (match-lambda (#t (quote 't))
+                (#f (quote 'nil))
+                (val val)))
+
+(define (emacs-variables var-alist)
+  "Converts an alist of variable names and values into a @code{setq}
+expression that can be used in an Emacs configuration.  Scheme values
+@code{#t} and @code{#f} will be converted into @code{t} and @code{nil},
+respectively."
+  #~(string-append
+     "(setq"
+     #$@(map (lambda (var)
+               #~(format #f "\n  ~a ~s"
+                         (quote #$(car var))
+                         #$(scheme-value->emacs-value (cdr var))))
+             var-alist)
+     ")\n\n"))
diff --git a/gnu/local.mk b/gnu/local.mk
index 184f43e753..35d88b4dd6 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -89,6 +89,7 @@  GNU_SYSTEM_MODULES =				\
   %D%/home/services.scm			\
   %D%/home/services/desktop.scm			\
   %D%/home/services/symlink-manager.scm		\
+  %D%/home/services/emacs.scm			\
   %D%/home/services/fontutils.scm		\
   %D%/home/services/guix.scm			\
   %D%/home/services/pm.scm			\