Neovim window editing a Leptos file with autocompletion, intellisense, and all the goodiesNeovim window editing a Leptos file with autocompletion, intellisense, and all the goodies

There's a subtle pleasure in having your editor setup just the way you'd like. When you've got good error messages, autocompletion, intellisense, and all the LSP goodies we as developers have come to expect. Because we aren't programming in Notepad anymore. I wanted to know if I could get the same experience writing Leptos components that I do writing React ones. And the answer is yes!

Setting it all up will require a little bit of work, but in the end we should have a Leptos IDE rivaling that of Javascript in Visual Studio Code!

Lunarvim

I've decided to use Lunarvim here because it's most of the way there, and it's what I have installed. There's no reason this couldn't work in Astrovim, LazyVim, or NvChad, which are all based on Neovim. Using it lets me skip setup for a bunch of IDE and plugin setup that isn't done in base Neovim.

Besides Neovim, most of this is replicable in VS Code, Helix, Zed, or your editor of choice. Check out the Leptos DX Docs for more detail on those. Unfortunately, it's not possible to get any of these to be quite as nice as Neovim currently.

We should have a Leptos and Rust compatible Lunarvim setup in less than ten minutes! Let's jump in.

Prerequisites

  1. Install rust via rustup, cargo-leptos, and leptosfmt

    As far as Rust installation goes, we need to add the wasm32-unknown-unknown target, and if you're choosing to use nightly, install that as well.

    1. Install rust via the instructions here for your platform
      1. Install the new rust target
      bash
      rustup target add wasm32-unknown-unknown
      
      1. Install nightly(Optional)
      bash
      rustup default nightly
      
      1. Rerun step 2 for nightly, as targets are installed per rust version

    Leptos uses cargo-leptos to simplify setup for SSR apps. If you'd like to focus on CSR apps or think you might in the future, feel free to add trunk to the list below.

    bash
    cargo install cargo-leptos 
    
  2. Install Lunarvim with their instructions

Configuration

General Lunarvim configuration is done by default in your ~/.config/lvim/config.lua. This is where you can both add plugins and use traditional neovim configuration

Rust Analyzer

Leptos has always been a bit tricky for rust-analyzer, primarily because we have different bits of the app behind different feature flags, and use some macros by default.

We can tell r-a to open the settings for rust by running

lua
:LspSettings rust_analyzer

or

lua
:LspSettings buffer

if you have a Rust file opened.

in Lunarvim. This should ask if you'd like to create an LSP config folder at ~/.config/lvim/lsp-settings, which we definitely do. After that it should show us our LSP settings, which are blank for a new install. Below I have my LSP settings with comments on what they do. Remember to remove the -- lines, as JSON does not allow comments, or copy it from the uncommented version at the end of the post.

JSON
{
"rust-analyzer": {
   -- Run command on save to check file. Defaults to cargo check, here I'm enabling clippy
   -- allFeatures so that it checks the whole file
  "checkOnSave":{
      "allFeatures": true,
      "command": "clippy"
    },
   -- Enable proc macro expansion and ignore the server macro
  "procMacro": {
    "enable": true,
    "ignored": {
      "leptos-macro": ["server"]
      }
    },
  -- Enable all features in the project. Because Leptos defaults to enabling hydrate, the server code will be commented out without this
  "cargo": {
    "allFeatures": true
  },
  -- Discussed more below, this'll run the Leptos formatter alongside rustfmt
  "rustfmt": {
    "overrideCommand": ["leptosfmt", "--stdin", "--rustfmt"]
  }
  },
  "rust-analyzer.callInfo.full": true,
  "rust-analyzer.lens.enable": true,
  -- Enable inlay hints
  "rust-analyzer.inlayHints.enable": true,
  "rust-analyzer.inlayHints.typeHints": true,
  "rust-analyzer.inlayHints.parameterHints": true,
  -- Enable CodeLens and its various sub things
  "rust-analyzer.lens.references": true,
  "rust-analyzer.lens.implementations": true,
  "rust-analyzer.lens.enumVariantReferences": true,
  "rust-analyzer.lens.methodReferences": true,
  -- Reload rust-analyzer if the Cargo.toml/Cargo.lock file changes
  "rust-analyzer.cargo.autoreload": true,
  -- Hover Actions!
  "rust-analyzer.hoverActions.enable": true
}


For all the things that can be configured, check out the rust-analyzer docs here.

Code Formatting

Rust includes rustfmt, which happily and consistently formats Rust code into a consistent style. It, however, does not know how to format HTML or RSX in our view! macros. So we'll need to augment it with leptosfmt, which does. Thanks @bram209! Let's install it with

bash
cargo install leptosfmt

I like to automatically run leptosfmt before rustfmt for all Rust projects, since leptosfmt only works on stuff in the view! macros and its run time is inconsequential. That can be configured with the below setting in your LSP settings.

JSON
"rust-analyzer": {
	"rustfmt": {
    	"overrideCommand": ["leptosfmt", "--stdin", "--rustfmt"]
	}
}

I also enable format on save by adding the below to config.lua:

lua
lvim.format_on_save.enabled = true

You could choose to enable it on a per project basis, I'll leave that as an exercise for the reader.

Treesitter Grammar

Neovim uses treesitter to parse source code and generate a syntax tree that can be used for syntax highlighting and helpful functions based on the code.

There's a treesitter grammar for Rust, so that it'll work well, but it's not setup to recognize html and css in a Rust file. For that, we'll need a new treesitter grammar.

Thanks to Leptos user @rayliwell, we now have one in the form of tree-sitter-rstml. It's available as a neovim plugin and can be installed via your package manager of choice. For lunarvim, we add it to the plugins list in our config.lua file.

lua
lvim.plugins = {
	{
    "rayliwell/tree-sitter-rstml",
    dependencies = { "nvim-treesitter" },
    build = ":TSUpdate",
    config = function()
      require("tree-sitter-rstml").setup()
    end
	},
}

Once installed, you should see the HTML being smartly highlighted in view macros, similar to below:

Rust code
[component]
pub fn Blog() -> impl IntoView {
    let add_post = create_server_multi_action::<AddPost>();
	// The stuff in the view macro is the key bit here
	// It should recognize the on:click handler, and have different 
	// colors for the various HTML attribbutes
	view!{
		  <div class="grid min-h-full w-full grid-cols-2">
                <section class="text-left flex-col w-full">
                    <div on:click=move |_| {
                        show_post_metadata.update(|b| *b = !*b);
                    }>
					//...

                       
    }
}
                        

Autoclosing Tags

Maybe I'm a bit spoiled, but I've gotten used to the IDE autoclosing HTML tags when I write them, like this:

Thankfully @rayliwell has come to the rescue once again with a fork of nvim-ts-autotags to solve that problem as well. It can be enabled by adding it to our config.lua lvim.plugins list as we did before.

lua
lvim.plugins = {
 {
    "rayliwell/nvim-ts-autotag",
    config = function()
      require("nvim-ts-autotag").setup()
    end,
  },
}

Additional Plugins

There are so many great plugins for neovim, more than I'll probably ever be able to find, learn, and use. I've listed some of the ones I use and like below that are not preinstalled with Lunarvim. Check out that list if you're starting from scratch with neovim:

Conclusion

Hopefully this helps you with your editor configuration for Leptos. I know Neovim can be an adjustment if you're not used to it. I remain hopeful that Rust Rover, Helix, and Visual Studio Code catch up to the experience. If you have any questions, neovim plugin recommendations, or corrections, feel free to reach out on Mastodon at @[email protected].

Complete Files

lua
-- ~/.config/lvim/config.lua
lvim.plugins = {
  {
    "nvim-pack/nvim-spectre",
    event = "BufRead",
    config = function()
      require("spectre").setup()
    end,
  },
  {
    "tpope/vim-fugitive",
    cmd = {
      "G",
      "Git",
      "Gdiffsplit",
      "Gread",
      "Gwrite",
      "Ggrep",
      "GMove",
      "GDelete",
      "GBrowse",
      "GRemove",
      "GRename",
      "Glgrep",
      "Gedit"
    },
    ft = { "fugitive" }
  },
  {
    "j-hui/fidget.nvim",
    opts = {
      -- options
    }
  },
  {
    'windwp/nvim-autopairs',
    event = "InsertEnter",
    config = true
    -- use opts = {} for passing setup options
    -- this is equalent to setup({}) function
  },
  {
    "rayliwell/tree-sitter-rstml",
    dependencies = { "nvim-treesitter" },
    build = ":TSUpdate",
    config = function()
      require("tree-sitter-rstml").setup()
    end
  },
  {
    "rayliwell/nvim-ts-autotag",
    config = function()
      require("nvim-ts-autotag").setup()
    end,
  },
  {
    "folke/trouble.nvim",
      cmd = "TroubleToggle",
  },
   {
    "iamcco/markdown-preview.nvim",
    cmd = { "MarkdownPreviewToggle", "MarkdownPreview", "MarkdownPreviewStop" },
    ft = { "markdown" },
    build = function() vim.fn["mkdp#util#install"]() end,
},
{
  "sindrets/diffview.nvim",
  event = "BufRead",
},
}

lvim.format_on_save.enabled = true
lvim.builtin.which_key.mappings["t"] = {
  name = "Diagnostics",
  t = { "<cmd>TroubleToggle<cr>", "trouble" },
  w = { "<cmd>TroubleToggle workspace_diagnostics<cr>", "workspace" },
  d = { "<cmd>TroubleToggle document_diagnostics<cr>", "document" },
  q = { "<cmd>TroubleToggle quickfix<cr>", "quickfix" },
  l = { "<cmd>TroubleToggle loclist<cr>", "loclist" },
  r = { "<cmd>TroubleToggle lsp_references<cr>", "references" },
}

vim.opt.relativenumber = true

~/.config/lvim/lsp-settings/rust_analyzer.json

JSON
{
"rust-analyzer": {
  "checkOnSave":{
      "allFeatures": true,
      "command": "clippy"
    },
  "procMacro": {
    "enable": true,
    "ignored": {
      "leptos-macro": ["server"]
      }
    },
  "cargo": {
    "allFeatures": true
  },
  "rustfmt": {
    "overrideCommand": ["leptosfmt", "--stdin", "--rustfmt"]
  }
  },
  "rust-analyzer.callInfo.full": true,
  "rust-analyzer.lens.enable": true,
  "rust-analyzer.inlayHints.enable": true,
  "rust-analyzer.inlayHints.typeHints": true,
  "rust-analyzer.inlayHints.parameterHints": true,
  "rust-analyzer.lens.references": true,
  "rust-analyzer.lens.implementations": true,
  "rust-analyzer.lens.enumVariantReferences": true,
  "rust-analyzer.lens.methodReferences": true,
  "rust-analyzer.cargo.autoreload": true,
  "rust-analyzer.hoverActions.enable": true
}