Posts

  • nREPL 1.1.1: Improved Completion with compliment-lite

    Today I’ve released nREPL 1.1.1 with a couple of small bug-fixes and one more notable, if mostly invisible change.

    Historically nREPL’s completions op (introduced in nREPL 0.8) used internally a modified version of clojure-complete, that I eventually released as a library named incomplete. clojure-complete was pretty much abadonware at this point and I wanted a simple drop-in replacement that we could use in tools like REPLy (and by association - Leiningen).1

    Using the much better compliment library wasn’t an option, as we needed something that was quite literally a single file. (so it’d be easy to inline in nREPL and load in REPLy) Recently, however, compliment’s author Oleksandr Yakushev released exactly what we needed - a single-file, stripped-down version of compliment called compliment-lite. Here’s how it differs from the full-fledged compliment:

    • Context. Completion features that require context (e.g., filtering methods by the class of the receiver object) don’t work.
    • Local bindings and resources. Those two sources of completions completely rely on context, so they are disabled in Compliment-lite.
    • Documentation. The documentation function is absent from Compliment-lite.

    None of those were things supported by incomplete, so nothing lost. Naturally, it made sense for nREPL to adopt it and offer improved out-of-the-box completion experience to Clojure programmers. As this doesn’t affect the public API of nREPL2 I’ve opted to ship this change in a “bug-fix” release. A version like 1.1.1 is just too cool to pass up!

    In light of the creation of compliment-lite I now consider incomplete to be obsolete and I’d encourage everyone looking for a simple, but capable code-completion library to go for compliment-lite instead.

    That’s all I have for you today. Keep hacking!

  • Need for Speed: Using RuboCop with Prism

    By now probably most Rubists have heard of Prism (formerly know as YARP), the modern and (very) fast Ruby parser that’s aiming to replace all the existing Ruby parsers out there. Including RuboCop’s “own” parser (a.k.a whitequark/parser), which aimed to replace ripper and ruby_parser, back when it was created.

    I’ve been keeping an eye on Prism for while, as RuboCop has long been criticized for its choice of parser (and more specifically - for that parser being somewhat slow). That being said - parser migrations are always a pain, especially in a project as big as RuboCop. Early on I had to essentially rewrite RuboCop when I switched the codebase from ripper to parser, and back then RuboCop was a lot smaller. The good thing is that such rewrites can be done incrementally (I was migrating cops in batches), but it was still a lot of (boring, repetitive) work.

    That’s why I was super happy when I recently discovered parser-prism - a gem that provides a new backend for the parser gem’s syntax tree that uses the Prism parser. To make things better - it seemed that this library actually worked with RuboCop already. Yeah, the users still had to do some manual work, but it turned out that the migration could be a lot simpler than what I had expected. Did I also mention some mighty impressive benchmarks?


    As a whole, this parser should be significantly faster than the parser gem. The bin/bench script in this repository compares the performance of Parser::CurrentRuby and Parser::Prism. Running against a large file like lib/parser/prism/compiler.rb yields:

    Warming up --------------------------------------
     Parser::CurrentRuby     1.000  i/100ms
           Parser::Prism     6.000  i/100ms
    Calculating -------------------------------------
     Parser::CurrentRuby     16.642  (± 0.0%) i/s -     84.000  in   5.052021s
           Parser::Prism     64.951  (± 3.1%) i/s -    330.000  in   5.088147s
    
    Comparison:
           Parser::Prism:       65.0 i/s
     Parser::CurrentRuby:       16.6 i/s - 3.90x  slower
    

    When running with --yjit, the comparison is even more stark:

    Warming up --------------------------------------
     Parser::CurrentRuby     1.000  i/100ms
           Parser::Prism     9.000  i/100ms
    Calculating -------------------------------------
     Parser::CurrentRuby     20.062  (± 0.0%) i/s -    101.000  in   5.034389s
           Parser::Prism    112.823  (± 9.7%) i/s -    558.000  in   5.009460s
    
    Comparison:
           Parser::Prism:      112.8 i/s
     Parser::CurrentRuby:       20.1 i/s - 5.62x  slower
    

    These benchmarks were run on a single laptop without a lot of control for other processes, so take them with a grain of salt.


    Note: The results above were taken straight from parser-prism’s README. In a nutshell we’re looking into something like 4-6 times speedup!!! Now I was VERY excited!

    Immediately I created an issue to improve the support for Prism in RuboCop and we’ve started to collaborate with Prism’s author Kevin Newton, who has been very supportive and accommodating. I suggest to everyone interested in the topic to peruse the discussion in this issue (and issues references from it), as we’ve already managed to simplify the usage of RuboCop with Prism quite a bit. And we’re very close to addressing the main outstanding item - multi-versioning for the parser. Basically, the parser gem allows you to select the version of Ruby to parse code as (e.g. 3.1), independently from the version of Ruby runtime. This powers the TargetRubyVersion configuration option in RuboCop and was one of the many reasons for picking parser over ripper back in the day. In practical terms - this allowed us to keep supporting parsing Ruby 2.x code, long after one couldn’t run RuboCop on Ruby 2.x. To put it in different terms - the versions of Ruby that RuboCop understands (can parse) are completely orthogonal to the versions of Ruby on which RuboCop can be run.

    This and other items we need to address, are nicely summarized here. None of them seems like a particularly hard obstacle, so I’m quite optimistic about the future. A future in which one day you’ll be able to have this in your RuboCop config:

    ParserEngine: prism
    

    When is this going to happen exactly? No promises yet, but at the current pace it will likely happen sooner rather than later. I’d encourage adventurous people to play with RuboCop on Prism and to contribute to getting all required pieces in place faster.

    I recall that RuboCop’s adoption of whitequar/parser was instrumental in uncovering (many) weird bugs in it, and I have a feeling that things might be the same for parser-prism as well. (I assume a lot less projects use parser-prism compared to Prism) Exciting times ahead!

    That’s all I have for you today! Keep hacking!

  • Configuring fixed/tonsky indentation in clojure-mode

    A few years ago Nikita Tonsky made popular a certain style of formating Clojure code, that became known as “fixed” or “tonsky” indentation in the community.

    clojure-mode has long had some support for this via clojure-indent-style:

    (setq clojure-indent-style 'always-indent)
    

    However, it was kind of hard to get exactly the same indentation from Nikita’s article, as there was no way to suppress the usage of indent specs and forms starting with a keyword were indented by separate rules from what was set by clojure-indent-style. A recent upstream change made the indentation configuration more granular and now you can get fixed indentation with the following snippet:

    (setq clojure-indent-style 'always-indent
          clojure-indent-keyword-style 'always-indent
          clojure-enable-indent-specs nil)
    

    clojure-indent-keyword-style and clojure-enable-indent-specs are the new additions, that made this possible. Here’s how clojure-indent-keyword-style can be used:

    • always-align (default) - All args are vertically aligned with the first arg in case (A), and vertically aligned with the function name in case (B).

       (:require [foo.bar]
                 [bar.baz])
       (:require
        [foo.bar]
        [bar.baz])
      
    • always-indent - All args are indented like a macro body.

        (:require [foo.bar]
           [bar.baz])
        (:x
           location
           0)
      
    • align-arguments - Case (A) is indented like always-align, and case (B) is indented like a macro body.

        (:require [foo.bar]
                  [bar.baz])
        (:x
           location
           0)
      

    By the way, clojure-ts-mode also supports the fixed indentation style:

    (setq clojure-ts-indent-style 'fixed)
    

    The configuration here is a lot simpler, as this functionality existed since day 1.

    For the record, I still don’t endorse/like fixed indentation1 (and funny enough - neither does Arne, who implemented those changes). Still, I acknowledge that this style is somewhat popular in the Clojure community and I’m all for making it easier for people who like/need it to be able to use it.

    That’s all I have for you today. Keep hacking!

  • Flycheck Updates: A new Stable Release and Way More

    I’ve been pretty busy with Flycheck ever since I became its maintainer recently. Today I wanted to share with you a few of the recent highlights.

    Flycheck 34

    I’ve cut Flycheck 34, which features quite a few new checkers and removes a bunch of legacy checkers. As I’m not familiar with all the lint tools that Flycheck supports1, I’d appreciate your input on whether some checkers need to be updated/replaced/removed down the road.

    Random trivia: I noticed that Chef’s foodcritic has been replaced by cookstyle, a project built on top of my very own RuboCop project (a linter & formatter for Ruby) and I wrote the checker for it myself.

    flycheck.org Ownership

    I’ve managed to obtain the ownership of the flycheck.org domain. Big thanks to Matthias Güdemann for paying for the domain after the departure of Sebastian from Flycheck.

    flycheck-eglot

    I quickly realized that Eglot support was one of the main reasons why people switched from Flycheck to Flymake, so when I noticed that Sergey Firsov had created Flycheck backend for Eglot I immediately invited him to move his project to the official Flycheck organization on GitHub. You can find flycheck-eglot here.

    Documentation Updates

    I’ve been updating the Flycheck docs here and there. Most notably, I’ve updated the comparison with Flymake, as a few people complained it was out-of-date.

    I haven’t used Flymake in ages, so I’m definitely not an expert there, but I did my best to make sure the comparsion is reasonably accurate. If someone notices any issues there - please, submit improvements!

    Open Collective

    I’ve created an Open Collective for Flycheck, so people and organizations can support the project financially via donations. I hope that in the long-run it can make the maintenance of the project more sustainable. In the short run - I’ll use some of the donations to cover my expenses for flycheck.org.

    NonGNU ELPA

    I’ve filed a request to submit Flycheck to NonGNU ELPA, one of Emacs’s official package repositories. Let’s see how this will go. I’m hoping to have there Flycheck and many of its extensions in the long run.

    Update: You can track the emacs-devel discussion on the topic here.

    Help Welcome

    If you want to hack on Flycheck or any of its extensions - now would be a great time! I love working with other contributors and Flycheck could definitely use all the help it could get. The main organization features some 20 projects and not all of them are actively maintained today.

    Epilogue

    And that’s a wrap! It has been a busy few weeks working on Flycheck, but now I’ve done pretty much everything that I wanted to tackle originally. Moving forward I’ll be working slowly on the backlog of issues and pull requests and pondering whether more ambitious changes will be needed down the road.

    Thanks to everyone for their support for the project! Keep hacking!

    1. See https://www.flycheck.org/en/latest/languages.html for the list of built-in checkers. 

  • CIDER: Preliminary Support for clojure-ts-mode

    I’m glad to report that yesterday the long-awaited preliminary support for clojure-ts-mode in CIDER has landed!1

    This pull request (or rather the original PR on which it was based) was in the works for a very long time and it feels good to see it finally merged. What does this mean in practice? Well, CIDER will now properly recognize clojure-ts-mode and modes derived from it, meaning most of the functionality in CIDER will work reasonably well with them. There are a few caveats to keep in mind, though:

    • CIDER still has a hard dependency on clojure-mode, as it relies on some APIs from it that have yet to be ported to clojure-ts-mode
    • Some functionality like dynamic indentation and dynamic font-locking (syntax highlighting) is still not support by clojure-ts-mode
    • You need to use the latest version of clojure-ts-mode, as it features some related changes
    • The new code hasn’t been test much, so probably we’ll encounter some bugs along the way
    • You’ll need to use a snapshot release of CIDER (e.g. one installed from MELPA), as there’s no stable CIDER release with this functionality yet (CIDER 1.14 will be the first one)
    • You need to be on Emacs 29 to be able to use clojure-ts-mode in the first place

    At any rate - we’ve made one big step towards decoupling CIDER from clojure-mode and gradually we’ll get there. Thanks to everyone who was involved in making this happen! Keep hacking!

    1. I’d suggest checking out https://metaredux.com/posts/2023/03/12/clojure-mode-meets-tree-sitter.html if you’re not familiar with clojure-ts-mode

Subscribe via RSS | View Older Posts