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
-
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.- Install rust via the instructions here for your platform
- Install the new rust target
bashrustup target add wasm32-unknown-unknown
- Install nightly(Optional)
bashrustup default nightly
- 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.bashcargo install cargo-leptos
- Install rust via the instructions here for your platform
-
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
:LspSettings rust_analyzer
or
: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.
{ "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
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.
"rust-analyzer": { "rustfmt": { "overrideCommand": ["leptosfmt", "--stdin", "--rustfmt"] } }
I also enable format on save by adding the below to config.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.
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:
[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.
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:
- vim-fugitive - Inbuilt Git Handler
- diffview - More easily see changes in a git diff
- fidget - Ever wonder what the LSP is doing? Well now you'll know
- nvim-spectre - Better search/replace for Neovim
- trouble - Handy list of all your errors
- markdown-preview - Preview Markdown in the browser
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
-- ~/.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
{ "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 }