Language Server

September 2019

In this article we are discussing a concrete setup with concrete tools and a concrete language. But the solutions described here, are intended to help you with your set up.

Setup

Here are the Tools used.

Goal

This way project specific tweaks are stored in the place where it belongs, and other people can use their favorite IDE with the same setup.

Configure Spacemacs

Spacemacs is basically an ~/.emacs.d folder and a mutable file ~/.spacemacs. I tried to configure ~/.spacemacs via home-manager but this does not play well with updates and with customization.

Now I’m using home-manager to configure files in ~/.spacemacs.d/ and load them in the configuration functions inside ~/.spacemacs. A simple (load "~/.spacemacs.d/hook-user-config.el") inside the dotspacemacs/user-config function is enough, to make it work.

{ pkgs, lib, config, ... }:
let
  user = "mainUser";
  userName = config.users.users."${user}".name;
  home = config.users.users."${user}".home;
  fontSize = 14;

  startupBanner = pkgs.fetchurl{
    url = "https://github.com/NixOS/nixos-homepage/raw/master/logo/nix-wiki.png";
    sha256 = "1hrz7wr7i0b2bips60ygacbkmdzv466lsbxi22hycg42kv4m0173";
  };
in {
  systemd.services =
    let
      clone =
        repository: folder: branch:
        {
          enable = true;
          wantedBy = [ "multi-user.target" ];
          description = "clone ${repository} to ${folder}";
          serviceConfig.User = userName;
          unitConfig.ConditionPathExists = "!${folder}";
          script = ''
            ${pkgs.git}/bin/git clone ${repository} --branch ${branch} ${folder}
          '';
        };
    in
    {
      emacs-pull = clone "https://github.com/syl20bnr/spacemacs" "${home}/.emacs.d" "master";
    };

  home-manager.users."${user}" = {

    home.file.".spacemacs.d/hook-init.el".text = ''
      ;; just add (load "~/.spacemacs.d/hook-init.el")
      ;; at the end of dotspacemacs/init function
      ;; overrides of dotspacemacs/init ()
      (setq-default
       dotspacemacs-themes '(solarized-light solarized-dark)
       dotspacemacs-startup-banner "${startupBanner}"
       dotspacemacs-default-font 
       '("Terminus" :size ${toString fontSize}
                    :weight normal
                    :width normal
                    :powerline-scale 1.1))
    '';

    home.file.".spacemacs.d/hook-layers.el".text = ''
      ;; just add (load "~/.spacemacs.d/hook-layers.el") 
      ;; at the end of dotspacemacs/layers function
      (let
        ((user-layers dotspacemacs-configuration-layers))
        (setq
          dotspacemacs-configuration-layers
            (append user-layers
              '( spell-checking
                 syntax-checking
                 (haskell :variables
                    haskell-enable-hindent t
                    haskell-completion-backend 'lsp
                    haskell-enable-hindent-style "gibiansky"
                    haskell-process-type 'cabal-new-repl)
               ))))
      (let
        ((user-packages dotspacemacs-additional-packages ))
        (setq
          dotspacemacs-additional-packages
            (append user-packages
              '( lsp-mode
                 lsp-ui
                 lsp-haskell
                 direnv
              ))))
    '';

    home.file.".spacemacs.d/hook-user-config.el".text = ''
      ;; just add (load "~/.spacemacs.d/hook-user-config.el") 
      ;; at the end of dotspacemacs/user-config function

      ;; lsp setup for haskell
      ;; hie-wrapper must be installed and configured in the direnv setup
      (setq lsp-haskell-process-path-hie "hie-wrapper")
      (setq lsp-response-timeout 60)
      (require 'lsp-haskell)
      (add-hook 'haskell-mode-hook #'lsp)
      (add-hook 'haskell-mode-hook #'direnv-update-environment)
    '';
  };
}

We setup emacs to run direnve-update-environment and lsp once we start the haskell-mode. But we did not install lsp. In my setups the lsp-server is installed by the project file (lsp.nix), and is loaded via direnv (direnv-update-environment in emacs). If you don’t like that just use the snippet from the next section.

Alternative Configuration (install lsp in the configuration.nix)

You can install the lsp (in our case hie-wrapper) globally in your configuration.nix . I usually do this in my projects (via lsp.nix). Here is the part that differs.

home.file.".spacemacs.d/hook-user-config.el".text =
  let
    all-hies = import (fetchTarball "https://github.com/infinisil/all-hies/tarball/master") {};
  in ''
    ;; just add (load "~/.spacemacs.d/hook-user-config.el") 
    ;; at the end of dotspacemacs/user-config function

    ;; lsp setup for haskell
    (setq lsp-haskell-process-path-hie 
      "${all-hies.selection{ selector = p: { inherit (p) ghc864;}; } }/bin/hie-wrapper")
    (setq lsp-response-timeout 60)
    (require 'lsp-haskell)
    (add-hook 'haskell-mode-hook #'lsp)
    (add-hook 'haskell-mode-hook #'direnv-update-environment) ;; still needed
  '';

Setup the project

For a Haskell project I have this minimal setup of files.

lsp.nix

This file is to setup the lsp-server. If you already installed the lsp-server via the configuration.nix, this file is not necessary, but also does not hurt.

{ pkgs ?  import <nixpkgs> {} }:
let
  all-hies = import (fetchTarball "https://github.com/infinisil/all-hies/tarball/master") {};
in
pkgs.mkShell {
  buildInputs = with pkgs; [
    haskellPackages.hoogle
    haskellPackages.hindent
    haskellPackages.hlint
    haskellPackages.stylish-haskell
    (all-hies.selection { selector = p: {inherit (p) ghc864; }; })
  ];
}

env.nix

Provides the environment to run cabal test and cabal build. All package files (e.g. ./current-project.nix) are created by cabal2nix.

{ pkgs ?  import <nixpkgs> {
  overlays = [
    (self: super: {
      haskellPackages = super.haskellPackages.override {
        overrides = self: super: {
          datetime = super.callPackage ./datetime.nix {};
          current-project = super.callPackage ./current-project.nix { };
        };
      };
    })];
}}:
pkgs.haskellPackages.current-project.env

shell.nix

For other scripts and tooling important for development.

{ pkgs ?  import <nixpkgs> {} }:
let
  updateCabal = pkgs.writeShellScriptBin "update-cabal" /* sh */ ''
    echo "# created by cabal2nix " > ${toString ./.}/current-project.nix
    ${pkgs.cabal2nix}/bin/cabal2nix ${toString ./.} >> ${toString ./.}/current-project.nix
  '';
in
pkgs.mkShell {
  buildInputs = with pkgs; [
    updateCabal
    openapi-generator-cli
    openssl
    cabal2nix
  ];
}

.envrc

finally we need a direnv configuration file. direnv and the direnv-mode make it possible to load the environment needed and provided by the *.nix files.

use nix ./env.nix
use nix ./lsp.nix
use nix ./shell.nix

Don’t forget to run direnv allow in the project folder.

Conclusion

Now we are capable to use the lsp-server configured in all our projects, with the editor we prefer. Your colleagues will have little problems with the setup and improve it.

If you prefer to install the lsp globally, you can simply do that as described, this will not interfere with the lsp server setup in the lsp.nix file.

I’m running this setup for quite a while now, and I experience little to no problems with it. The most common thing is that I have to fire lsp-restart-workspace to remove old errors, but doing this every hour is not a problem for me.

Support

If you have comments or problems just ping me palo @ irc.freenode.net