[Unfinished] Suffering

We keep romanticizing suffering. That's all okay. But, if that's all we do about our suffering, we are going in the wrong direction.

Suffering is necessary to feel the joy from its absence. But what if suffering itself becomes joyful. We lose the incentive to overcome it.

We don't think about the end of the dark tunnel, the dawn after a dark stormy night.

We are content with the suffering itself because it's common among our peers. The good old sense of belonging wins over the will to rid ourselves of the pain.

That's why I prefer solitude. There's no other that I subconsciously mimic. There's no one I secretly love, envy or hate. I am an open book, when alone, albeit a messed up one.

A whole lot of us dislike change, including me. Mainly the ones that come unexpectedly, that we have no idea about.

eof

Published: 2024-06-08

Tagged: unfinished thought

Neovim as the Clojure PDE - II

Click here if you missed the first part.

Welcome back, folks. It's been a while since the first post of this series came out.

Today's agenda is to install some basic plugins and configure them. While doing that, we'll learn some fennel and have our own config in parallel.

Why Plugins?

Plugins enhance the raw editor by adding features that the raw Neovim doesn't provide out of the box. In VSCode world, we call them extensions.

We need a plugin that makes it easy for us to install and use other plugins. These are called Plugin Managers. There are many of them in Neovim/Vim world like Packer, Vim Plug, Lazy et al. For the purpose of this post and because it's the new shiny thing, we will use Lazy.

plugins.fnl

Just to recap, after our last post, the config directory looks like the following

$ tree -L 3 ~/.config/nvim
├── fnl
│   └── user
│       └── core.fnl
├── init.lua
└── lua
    └── user
        └── core.lua

We are going to add the following plugins now:

We'll put all the plugin installation related configuration in the plugins.fnl file. Let's start with installing Lazy, a lua based plugin manager for neovim.

;; fnl/user/plugins.fnl

;; Path to directory where lazy itself will be installed.
;; `(vim.fn.stdpath :data)` returns standard directory where nvim's data will be stored.
;; `..` is string concatenation in fnl.
(local lazy-path (.. (vim.fn.stdpath :data)
                     :/lazy/lazy.nvim))

;; `(vim.uv.fs_stat lazy-path)` returns nil if the `lazy-path` doesn't exist.
;; If the directory exists, it returns info about the directory.
(local lazy-installed? (vim.uv.fs_stat lazy-path))

;; We'll store plugins we need to install in this variable.
(local to-install [])

;; `setup` installs lazy plugin manager if not already installed, and also downloads the plugins added to `to-install`.
;; This is an exported function that is called from the parent module.
(fn setup []
  (when (not lazy-installed?)
    (vim.fn.system [:git
                    :clone
                    "--filter=blob:none"
                    "https://github.com/folke/lazy.nvim.git"
                    :--branch=stable
                    lazy-path]))
  (vim.opt.rtp:prepend lazy-path)
  (let [lazy (autoload :lazy)]
    (lazy.setup plugins-to-install)))

;; Exporting the setup function.
{: setup}

Let's import plugins.fnl in fnl/user/core.fnl and call the setup function.

;; fnl/user/core.fnl
(local plugins (require :user.plugins))

(fn setup []
  (plugins.setup))

{: setup}

In our init.lua, we need to call setup function of our core module.

-- init.lua
-- ...

require('user.core').setup()

Now, when we restart the neovim, the lazy plugin should be installed.

It is time for us to add the plugins we listed above.

;; fnl/user/plugins.fnl

;; ...
(local to-install
  [{1 :neovim/nvim-lspconfig
    :dependencies [{1 :williamboman/mason.nvim :config true} ;; Mason handles lsp servers for us and others
                   :williamboman/mason-lspconfig.nvim ;; default lspconfigs for language servers
                   {1 :echasnovski/mini.nvim ;; For lsp progress notification
                    :version false
                    :config (fn [] 
                              (let [mnotify (require :mini.notify)]
                                (mnotify.setup {:lsp_progress {:enable true
                                                               :duration_last 2000}})))}
                   :folke/neodev.nvim]} ;; neodev for lua neovim dev support.

   ;; Autocompletion   
   {1 :hrsh7th/nvim-cmp
    :dependencies [:hrsh7th/cmp-nvim-lsp]}

   ;; Fuzzy Finder (files, lsp, etc)
   {1 :nvim-telescope/telescope.nvim
    :version "*"
    :dependencies {1 :nvim-lua/plenary.nvim}}
   ;; Fuzzy Finder Algorithm which requires local dependencies to be built.
   ;; Only load if `make` is available. Make sure you have the system
   ;; requirements installed.
   {1 :nvim-telescope/telescope-fzf-native.nvim
    ;; NOTE: If you are having trouble with this installation,
    ;; refer to the README for telescope-fzf-native for more instructions.
    :build :make
    :cond (fn [] (= (vim.fn.executable :make) 1))}

   ;; Treesitter
   {1 :nvim-treesitter/nvim-treesitter
    :build ":TSUpdate"}
  
   ;; Conjure and related plugins
   :Olical/conjure
   :PaterJason/cmp-conjure ;; autocomplete using conjure
   :tpope/vim-dispatch
   :clojure-vim/vim-jack-in
   :radenling/vim-dispatch-neovim
   :tpope/vim-surround

   ;; Paredit
   {1 :julienvincent/nvim-paredit
    :config (fn []
              (let [paredit (require :nvim-paredit)]
                (paredit.setup {:indent {:enabled true}})))}
   ;; Paredit plugin for fennel
   {1 :julienvincent/nvim-paredit-fennel
    :dependencies [:julienvincent/nvim-paredit]
    :ft [:fennel]
    :config (fn []
              (let [fnl-paredit (require :nvim-paredit-fennel)]
                (fnl-paredit.setup)))}

   ;; Rainbow parens
   :HiPhish/rainbow-delimiters.nvim

   ;; Autopairs
   {1 :windwp/nvim-autopairs
    :event :InsertEnter
    :opts {:enable_check_bracket_line false}}])

Restart neovim, and we see Lazy UI installing the plugins added above.

Common configuration

Let's configure Neovim to solve some day-to-day pains eg. enabling relative line numbering, auto syncing system and neovim clipboards, some useful keymaps etc.

We'll create a general.fnl where we put all of this.

;; fnl/user/config/general.fnl

(fn setup []
  ;; disables highlighting all the search results in the doc
  (set vim.o.hlsearch false)

  ;; line numbering
  (set vim.wo.number true)
  (set vim.wo.relativenumber true)

  ;; disable mouse
  (set vim.o.mouse "")

  ;; clipboard is system clipboard
  (set vim.o.clipboard :unnamedplus)

  ;; Some other useful opts. `:help <keyword>` for the help.
  ;; For example: `:help breakindent` will open up vimdocs about `vim.o.breakindent` option.
  (set vim.o.breakindent true)
  (set vim.o.undofile true)
  (set vim.o.ignorecase true)
  (set vim.o.smartcase true)
  (set vim.wo.signcolumn :yes)
  (set vim.o.updatetime 250)
  (set vim.o.timeout true)
  (set vim.o.timeoutlen 300)
  (set vim.o.completeopt "menuone,noselect")
  (set vim.o.termguicolors true)
  (set vim.o.cursorline true)

  ;; Keymaps
  (vim.keymap.set [:n :v] :<Space> :<Nop> {:silent true})

  ;; To deal with word wrap
  (vim.keymap.set :n :k "v:count == 0 ? 'gk' : 'k'" {:expr true :silent true})
  (vim.keymap.set :n :j "v:count == 0 ? 'gj' : 'j'" {:expr true :silent true})

  ;; Tabs (Optional and unrelated to this tutorial, but helpful in handling tab movement)
  (vim.keymap.set [:n :v :i :x] :<C-h> #(vim.api.nvim_command :tabprevious)
                  {:silent true})
  (vim.keymap.set [:n :v :i :x] :<C-l> #(vim.api.nvim_command :tabnext)
                  {:silent true})
  (vim.keymap.set [:n :v :i :x] :<C-n> #(vim.api.nvim_command :tabnew)
                  {:silent true}))

;; exports an empty map
{: setup}

Plugin Configurations

It is time for us to configure each of these plugins to cater to our needs. These needs are plugin specific. We may want to add keymaps for commands we use often or tell Mason to make sure that particular LSP is always installed, or anything else that the plugin supports. These settings and configs can often be found on the plugin's github repository and neovim's documentation.

LSP configuration

;; fnl/user/config/lsp.fnl

(local nfnl-c           (require :nfnl.core))
(local ts-builtin       (require :telescope.builtin))
(local cmp-nvim-lsp     (require :cmp_nvim_lsp))
(local mason-lspconfig  (require :mason-lspconfig))
(local lspconfig        (require :lspconfig))
(local neodev           (require :neodev))

;; This function will be executed when neovim attaches to an LSP server.
;; This basically sets up some widely used keymaps to interact with LSP servers.
(fn on-attach [_ bufnr]
  (let [nmap (fn [keys func desc]
               (vim.keymap.set :n keys func
                               {:buffer bufnr 
                                :desc   (.. "LSP: " desc)}))
        nvxmap (fn [keys func desc]
                 (vim.keymap.set [:n :v :x] keys func
                                 {:buffer bufnr :desc (.. "LSP: " desc)}))]
    (nmap :<leader>rn vim.lsp.buf.rename "[R]e[n]ame")
    (nmap :<leader>ca vim.lsp.buf.code_action "[C]ode [A]ction")
    (nmap :gd vim.lsp.buf.definition "[G]oto [D]efinition")
    (nmap :gr (fn [] (ts-builtin.lsp_references {:fname_width 60}))
          "[G]oto [R]eferences")
    (nmap :gI vim.lsp.buf.implementation "[G]oto [I]mplementation")
    (nmap :<leader>D vim.lsp.buf.type_definition "Type [D]efinition")
    (nmap :<leader>ds ts-builtin.lsp_document_symbols "[D]ocument [S]ymbols")
    (nmap :<leader>ws
          (fn [] (ts-builtin.lsp_dynamic_workspace_symbols {:fname_width 60}))
          "[W]orkspace [S]ymbols")
    (nmap :K vim.lsp.buf.hover "Hover Documentation")
    (nmap :<C-k> vim.lsp.buf.signature_help "Signature Documentation")
    (nmap :gD vim.lsp.buf.declaration "[G]oto [D]eclaration")
    (nmap :<leader>wa vim.lsp.buf.add_workspace_folder
          "[W]orkspace [A]dd folder")
    (nmap :<leader>wr vim.lsp.buf.remove_workspace_folder
          "[W]orkspace [R]emove folder")
    (nmap :<leader>wl
          (fn []
            (nfnl-c.println (vim.inspect (vim.lsp.buf.list_workspace_folders))))
          "[W]orkspace [L]ist folders")
    (nvxmap :<leader>fmt vim.lsp.buf.format
            "[F]or[m]a[t] the current buffer or range")))

;; This binding keeps a map from lsp server name and its settings.
(local servers
       {:clojure_lsp            {:paths-ignore-regex :conjure-log-*.cljc}
        :lua_ls                 {:Lua {:workspace {:checkThirdParty false}
                                       :telemetry {:enable false}}}
        :fennel_language_server {:fennel {:diagnostics  {:globals [:vim]}
                                          :workspace    {:library (vim.api.nvim_list_runtime_paths)}}}})

(local capabilities
       (cmp-nvim-lsp.default_capabilities (vim.lsp.protocol.make_client_capabilities)))

(fn setup []
  (neodev.setup)
  (mason-lspconfig.setup {:ensure_installed (nfnl-c.keys servers)})
  (mason-lspconfig.setup_handlers [(fn [server-name]
                                     ((. (. lspconfig server-name) :setup) 
                                        {: capabilities
                                         :on_attach on-attach
                                         :settings (. servers server-name)

                                         ;; This is required because clojure-lsp doesn't send LSP progress messages if the `workDoneToken` is not sent from client. It is an arbitrary string.
                                         :before_init
                                         (fn [params _]
                                           (set params.workDoneToken 
                                                "work-done-token"))}))]))

{: setup}

Autocompletion configuration

;; fnl/user/config/cmp.fnl
(local cmp (require :cmp))

(fn setup []
  (cmp.setup {:mapping (cmp.mapping.preset.insert 
                         {:<C-d>     (cmp.mapping.scroll_docs -4)
                          :<C-f>     (cmp.mapping.scroll_docs 4)
                          :<C-Space> (cmp.mapping.complete {})
                          :<CR>      (cmp.mapping.confirm {:behavior cmp.ConfirmBehavior.Replace
                                                           :select false})
                          :<Tab>     (cmp.mapping (fn [fallback]
                                                    (if
                                                      (cmp.visible)
                                                      (cmp.select_next_item)

                                                      ;; else
                                                      (fallback))))
                          :<S-Tab>   (cmp.mapping (fn [fallback]
                                                    (if 
                                                      (cmp.visible)
                                                      (cmp.select_prev_item)
                                                      
                                                      ;; else
                                                      (fallback))))}                        
                         [:i :s])
              :sources [{:name :conjure}
                        {:name :nvim_lsp}]}))

{: setup}

Paredit configuration

;; fnl/user/config/paredit.fnl

(local paredit      (require :nvim-paredit))
(local paredit-fnl  (require :nvim-paredit-fennel))

(fn setup []
  (paredit.setup {:indent {:enabled true}})
  (paredit-fnl.setup))

{: setup}

Rainbow delimiters configuration

;; fnl/user/config/rainbow.fnl

(local rainbow-delimiters (require :rainbow-delimiters))

(fn setup []
  (set vim.g.rainbow_delimiters
       {:strategy {"" rainbow-delimiters.strategy.local}
        :query    {"" :rainbow-delimiters}}))

{: setup}

Telescope configuration

;; fnl/user/config/telescope.fnl

(local telescope  (require :telescope))
(local builtin    (require :telescope.builtin))
(local themes     (require :telescope.themes))

(fn setup []
  (telescope.setup {:defaults {:mappings {:i {:<C-u> false :<C-d> false}}}})
  (telescope.load_extension :fzf)

  ;; keymaps
  (vim.keymap.set :n :<leader>? builtin.oldfiles
                  {:desc "[?] Find recently opened files"})
  (vim.keymap.set :n :<leader>fb builtin.buffers {:desc "[F]ind open [B]uffers"})
  (vim.keymap.set :n :<leader>/
                  (fn []
                    (builtin.current_buffer_fuzzy_find (themes.get_dropdown {:winblend 10
                                                                             :previewer false})))
                  {:desc "[/] Fuzzily search in current buffer"})
  (vim.keymap.set :n :<leader>ff builtin.find_files {:desc "[F]ind [F]iles"})
  (vim.keymap.set :n :<leader>fh builtin.help_tags {:desc "[F]ind [H]elp"})
  (vim.keymap.set :n :<leader>fw builtin.grep_string
                  {:desc "[F]ind current [W]ord"})
  (vim.keymap.set :n :<leader>fg builtin.live_grep {:desc "[F]ind by [G]rep"})
  (vim.keymap.set :n :<leader>fd builtin.diagnostics
                  {:desc "[F]ind [D]iagnostics"})
  (vim.keymap.set :n :<leader>fcf
                  (fn [] (builtin.find_files {:cwd (vim.fn.stdpath :config)}))
                  {:desc "[F]ind [C]onfing [F]iles"})
  (vim.keymap.set :n :<leader>fch builtin.command_history
                  {:desc "[F]ind in [C]ommands [H]istory"})
  (vim.keymap.set :n :<leader>fm builtin.marks {:desc "[F]ind in [M]arks"}))

{: setup}

Treesitter configuration

;; fnl/user/config/treesitter.fnl

(local configs (require :nvim-treesitter.configs))

(local ensure-installed [:clojure :lua :vimdoc :vim :fennel])

(fn setup []
  (configs.setup {:ensure_installed       ensure-installed
                  ;; Add languages to be installed here that you want installed for treesitter
                  :auto_install           true
                  :highlight              {:enable true}
                  :indent                 {:enable  false}}))

{: setup}

Gluing everything together

Let's actually call setup methods of all these modules in our core.fnl.

;; fnl/user/core.fnl
(local plugins          (require :user.plugins))
(local u-general        (require :user.config.general))
(local u-telescope      (require :user.config.telescope))
(local u-treesitter     (require :user.config.treesitter))
(local u-lsp            (require :user.config.lsp))
(local u-cmp            (require :user.config.cmp))
(local u-paredit        (require :user.config.paredit))
(local u-rainbow        (require :user.config.rainbow))

(fn setup []
  (plugins.setup)
  (u-general.setup)
  (u-telescope.setup)
  (u-treesitter.setup)
  (u-lsp.setup)
  (u-cmp.setup)
  (u-paredit.setup)
  (u-rainbow.setup))

{: setup}

And that should be it. I know, this blog is long and contains a lot of code. Most of it should be self explanatory once you get familiar with the fennel syntax and vim docs.

The configuration is inspired by kickstart.nvim. More extensive configuration can be found on my dotfiles.

If there's a bug somewhere in here or you have a doubt, feel free to open an issue here.

Published: 2024-02-29

Tagged: neovim clojure fennel lua text-editor

Neovim as the Clojure PDE - I

Hello, fellow text editor enthusiasts. Welcome to Abstract Symphony, meaning of which, nobody knows and has nothing to do with the post's agenda.

You may gently ask for the meaning of PDE, what the fuck does that mean?

Well, you don't have to be so rude. It's an abbreviation for Personalized Development Environment, coined by TJ, Neovim's marketing head.

Why Neovim?

You know, same old bullshit like speed, muscle memory, extension of your body/mind, spiritualism, nirvana, moksha and so on. Nothing serious. Jokes aside, the real reason was seeing how fast Prime was, in giving up on clojure.

Why Clojure?

Because I love my high functioning parens. I can lisp down, 100s of reasons why, but that's beyond the scope. This emotion is hosted on a solid and reliable foundation. I am sure, a dynamic and immutable relationship is what keeps us tied together so strongly. Please forgive me, for my love for punning (nil?). IYKYK.

init.lua

Neovim is a professional's tool. You need to deserve it, earn its respect, you know? Like Thor and his hammer.

Or you can just install neovim and get started. At first sight, it doesn't look like anything more than a trap that you can never get out of (trust me you can :q!). And no, that was not my failed attempt at putting an emojee.

Create an init.lua file in $HOME/.config/nvim directory. This will be the entrypoint for your neovim configuration. It's similar to the main method in a java/c/cpp projects, an entry-point for the program to run.

Go ahead and add a print("Hello, world!") to the file. Now, when you run nvim, you should see Hello, world! at the bottom-left of your screen. Congratulations, for your first configured neovim instance.

Leader and LocalLeader

Just like how each country needs good leaders to function properly, neovim is no different. You should define a leader and localleader according to your convenience. People usually choose <Space> or "," as their leader. I'll go ahead with "," as that is what I am used to.

But wait, what is the purpose of the leaders, you ask? Well, the main reason is the "plugins". Plugin writers are not aware of how you have configured your editor. They can't arbitrarily setup keybindings in their plugins, as they may conflict with your bindings.

Neovim API exposes options via vim.g, vim.o, vim.opt, vim.bo and vim.wo. We can control behaviour of Neovim by setting these options. vim.g is used for global scoped options. We can read more about them in Neovim's help text using :help vim.g or :help vim.o etc.

Let's set our globally scoped leader and localleader.

-- init.lua
vim.g.mapleader = ','
vim.g.maplocalleader = ','

NSFW: This article goes deeper into how leader/localleader is helpful.

We'll use Fennel as our config language

Although, Neovim officially supports lua as its configuration language, we will use Fennel. Because, we love our parens. And also, we like to struggle and learn.

Fennel transpiles into lua, so we need a Neovim plugin to transpile our fennel files into lua. Olical's nfnl does exactly that. We will update our init.lua to download nfnl. We will also add nfnl's path to neovim's runtime path, so that it can find nfnl plugin.

-- init.lua
-- ...

local nfnl_path = vim.fn.stdpath 'data' .. '/lazy/nfnl'

if not vim.uv.fs_stat(nfnl_path) then
  print("Could not find nfnl, cloning new copy to", nfnl_path)
  vim.fn.system({'git', 'clone', 'https://github.com/Olical/nfnl', nfnl_path})
end

vim.opt.rtp:prepend(nfnl_path)

require('nfnl').setup()

We define a local variable nfnl_path. It holds the directory path where we will download nfnl.

Let us create a directory to store our fennel config files. And also a core.fnl file inside that directory.

# Creates the directory
mkdir -p $HOME/.config/nvim/fnl/user

# Creates the file
touch $HOME/.config/nvim/fnl/user/core.fnl

Let's add a simple Hello, world! print form in our core.fnl.

;; fnl/user/core.fnl
(println "Hello, world! This is fennel config!")

When we restart our neovim instance, nothing happens. We should have seen a hello world message. We're missing a key nfnl config.

nfnl looks for a .nfnl.fnl file in a directory for configuration about which files to compile and how. Create a .nfnl.fnl file with empty config in $HOME/.config/nvim directory.

echo {} > $HOME/.config/nvim/.nfnl.fnl

When we restart our instance again, we still don't see anything printed. Well, there are a couple of things pending.

On opening core.fnl, a prompt says that .nfnl.fnl is not trusted. Press a to allow (mark it as trusted). This is because nfnl by default won't compile files in a directory unless we specifically allow it to. Once we allow and save the core.fnl file, by using :write command, a new file lua/user/core.lua gets generated with transpiled lua code corresponding to the core.fnl.

Let's require user/core module in init.lua

-- init.lua
-- ...
-- append this to the bottom of init.lua file.
require('user.core')

Now, when you restart neovim, it greets you with:

Hello, world! This is fennel config!

Hop on to the next part.

Published: 2024-01-26

Tagged: neovim clojure fennel lua text-editor

Archive