(Posts)

Extending Emmet in Emacs

Updated [ Emmet , Emacs , Deno ]
show, don’t tell https://github.com/P233/emmet2-mode

As a front-end developer, I rely on Emmet every day. I started using it over a decade ago, when it was still called “Zen Coding”. It has saved me a great deal of time and made my work much easier. Over the years, I had occasional ideas for improving it, such as expanding m--gutter to margin: var(--gutter);. I wrote each idea down when it came to me, and over time they grew into a strong wish to customize Emmet to my own taste.

My first attempt was Emmet Css Snippets, a Sublime Text package I created 11 years ago. It holds 200 to 300 snippets that follow Emmet’s abbreviation rules, with a few changes of my own, such as shorter abbreviations and new ones for pseudo-classes. I did not know any scripting language at the time, not even JavaScript, so I wrote every snippet by hand. It took a long time, but as a side benefit, I now remember CSS properties and values very well.

I used Sublime Text for only two years before switching to Emacs. In Emacs, I started using emmet-mode and made my own fork of it, which is no longer on GitHub. With the little Emacs Lisp I knew, I could only change the snippets, as I had done in Emmet Css Snippets, and could not go further.

About nine months ago, @manateelazycat, one of my favorite Emacs developers, introduced deno-bridge, which lets Emacs plugins use Deno as a back end. Better still, Deno can use any npm package. This was the chance I had been waiting for. Now I can have Emmet process my abbreviations in Emacs, intercept user input before it reaches Emmet and change the text Emmet returns.

So I created emmet2-mode, a minor mode for Emacs that enhances Emmet. I have used it for over eight months, and it works as I expected. It fits my needs exactly and saves me even more time than Emmet alone. Here is how I made it work.

Enhancing markup

I will start with markup. Emmet’s markup abbreviations are well designed, and I do not intend to change them, but I have added three features.

Expanding abbreviations from any position

I find it inconvenient that Emmet reads abbreviations only backward from the cursor. If I move the cursor back to edit an abbreviation, I then have to move the cursor to the end of the abbreviation again before expanding. I wanted the abbreviation detected automatically, so that I could expand it from any position. Instead of the extract function that Emmet provides, I wrote my own extraction function: a regular expression finds every possible abbreviation on the line, and the function picks the right one based on the cursor position. Take this abbreviation:

ul>li*5>h4{title placeholder}+p{content placeholder}

I can expand this abbreviation with the cursor anywhere in it, which makes editing the content much easier.

Support for CSS Modules in JSX

I also use CSS Modules every day, probably in all my projects. For this reason, expanding .abc to <div className={css.abc}></div> is much more useful to me than expanding it to <div class="abc"></div>. Here css is the name of my CSS Modules object; the name is configurable, as shown below.

Emmet 2.4.0, released in March 2023, added a similar feature that marks a CSS Modules class with a double dot: ..abc. In emmet2-mode, several classes go through a class names constructor, clsx() by default: .a.b.c expands to <div className={clsx(css.a, css.b, css.c)}></div>.

Both the CSS Modules object and the class names constructor can be set per project with directory variables. Create a .dir-locals.el file at the project root and add:

((web-mode . ((emmet2-css-modules-object . "style")
              (emmet2-class-names-constructor . "classnames"))))

Setting (emmet2-markup-variant . "solid") also makes emmet2-mode work with Solid.js.

CSS expansion in markup

I rarely use this feature, but it is quite handy. It was inspired by emmet-mode.

Inside a <style>|</style> element or a <div style="|"></div> attribute, abbreviations expand as CSS. Inside a JSX <div style={{|}} /> attribute, they expand as CSS-in-JS instead.

Enhancing CSS

Currently, I mostly write SCSS. The CSS enhancements are therefore really SCSS enhancements, and some of them are opinionated.

Shorthand aliases

Numeric font-weight shorthands:

// fw2 ->
font-weight: 200;
// fw7 ->
font-weight: 700;

Full width and full height shorthands:

// wf ->
width: 100%;
// hf ->
height: 100%;

I make myself add a z-index whenever I use absolute or fixed positioning:

// posa ->
position: absolute;
z-index: |;

// posf1000 ->
position: fixed;
z-index: 1000;

Other property and value abbreviations go through Emmet’s fixed snippet table. I plan to match them against CSS data instead, the way at-rules and pseudo-selectors are matched below.

Personal SCSS functions

I use two functions in all my projects: ms() from Modular Scale and rhythm() from rhythm-sass.

// t(2) ->
top: rhythm(2);
// fz(1) ->
font-size: ms(1);

These two abbreviations save me a lot of time.

Custom properties

CSS custom properties are now widely supported, so emmet2-mode expands them too:

// m--gutter ->
margin: var(--gutter);
// p--a--b--c ->
padding: var(--a) var(--b) var(--c);

camelCase alias

In Emmet, : or - separates a property from its value: ma expands to max-width: ;, but m:a expands to margin: auto;. I find : harder to type, and I would rather use it to expand pseudo-selectors. So I added a camelCase alias: mA is the same as both m:a and m-a.

// ma ->
max-width: ;
// mA == m:a == m-a ->
margin: auto;

Comma between abbreviations

I use the Dvorak keyboard layout, where a comma is much easier to type than +. In emmet2-mode, a comma joins abbreviations as + does:

t0,r0,b0,l0 == t0+r0+b0+l0

At-rules

I am always looking for ways to work faster, even when the saving is only a few characters:

@cs -> @charset
@kf -> @keyframes
@md -> @media

@us -> @use "|";
@in -> @if not | {}

Expanding pseudo-classes and pseudo-elements

As mentioned above, : now triggers pseudo-selectors: type : followed by two or three letters that identify the pseudo-class or pseudo-element.

// :be ->
&::before {
  |
}

// .class:fu ->
.class:focus {
  |
}

Functional pseudo-classes have short forms too:

// :n(:fc) ->
&:not(:first-child) {
  |
}

// .class:n(:fc,:lc):be ->
.class:not(:first-child):not(:last-child)::before {
  |
}

// :nc(2n-1) ->
&:nth-child(2n-1) {
  |
}

The lists of pseudo-selectors and CSS at-rules come from VS Code Custom Data.

That covers all the improvements I have made to Emmet. For more details, see the emmet2-mode repository. I want to thank the Emmet team: all of this builds on their hard work.