Skip to content

Files

Latest commit

36fb579 · Apr 20, 2025

History

History
11281 lines (9387 loc) · 350 KB

config.org

File metadata and controls

11281 lines (9387 loc) · 350 KB

Doom Emacs configuration

Docs

Code style

Most of the code here will follow a functional mindset using dash.el and s.el. I will prefer those functions over built in functions if they are nicer to use.

I often use my helper function (t!) for template strings.

Variable Naming

All my private functions have the prefix my. It’s the only prefix where you can assume that packages/emacs/doom won’t override it and vice-versa.

Schema

  • my-namespace Its not pretty, but it’s the only way to work with emacs-lisps global scope for everything.
    • my-namespace::sub-namespace
  • my:variable: Variable
  • my/function: Private function
    • my-namespace/function: Private function with namespace
  • my|function: Interactive function
    • my-namespace|function: Interactive function
  • my@function: Macro
  • my*hook: Hook function

Exceptions

There are only a few exceptions, which I’m keeping without the prefix for ease of writing/reading:

Setup

Tangle Headers

;; -*- lexical-binding: t -*-
;; -*- no-byte-compile: t; -*-
;; -*- no-byte-compile: t; -*-

Required Packages

Library packages

(package! ct)

Load Libraries

(require 'dash)
(require 's)
(require 'f)
(require 'noflet)
(require 'ct)
(require 'dash)
(require 's)
(require 'f)
(require 'noflet)
(require 'ct)

Load custom scripts

(-some->> (f-expand "~/.config/dotfiles/new/modules/scripts/src/hyma.el")
          (-id-when #'f-exists?)
          (load-file))

My Standard Library

Macros & Functions that extend my elisp standard library. If you copy any of my emacs-lisp code, you will need these functions.

Tangles to autoload.el

Macros

Template Literals

(template "2 = <<(+ 1 1)>>")
;;;###autoload
(defmacro template (text)
  "Template literals"
  (let ((pattern "<<\\(.*?\\)>>"))
    ;; The regexp matches anything between delimiters, non-greedily
    (with-temp-buffer
      (save-excursion (insert text))
      (let ((matches '()))
        (while (re-search-forward pattern nil t)
          (push (match-string 1) matches)
          (replace-match "%s" t t))
        `(format ,(buffer-string) ,@(reverse (mapcar 'read matches)))))))

;;;###autoload
(defalias 't! 'template)

Ignore Arguments

;;;###autoload
(defmacro my@ignore-args (fn)
  "Return function that ignores its arguments and invokes FN."
  `(lambda (&rest _rest)
     (funcall ,fn)))

Comment

Similar to the clojure comment form

(defmacro comment (&rest body)
  "Comment out one or more s-expressions."
  nil)

General

Noop

;;;###autoload
(defun my/noop (&optional args) nil)

Without undo

;;;###autoload
(defmacro my/without-undo (&rest forms)
  "Executes FORMS with a temporary buffer-undo-list that is discarded afterwards.
Taken from http://www.emacswiki.org/emacs/UndoCommands with some
modifications."
  `(let* ((buffer-undo-list)
          (modified (buffer-modified-p))
          (inhibit-read-only t))
     (unwind-protect
         (progn ,@forms)
       (set-buffer-modified-p modified)) ()))

String match or original

;;;###autoload
(defun my/s-match-or (regex x)
  "Return match groups or original"
  (interactive)
  (-if-let ((match (s-match regex x)))
      (cdr match)
    (list x)))

;;;###autoload
(defun my/s-match-or-1 (regex x)
  "Return 1st match group or original."
  (interactive)
  (-if-let ((match (s-match regex x)))
      (car (cdr match))
    x))

Buffer

Visual region

(defun my-buffer/selected-text ()
  "Get the currently selected text as a string.
Only takes evil visual selected text."
  (when (evil-visual-state-p)
    (buffer-substring-no-properties (region-beginning) (region-end))))

Check if buffer has line

(defun my-buffer/contains-line (string)
  (save-excursion
    (save-match-data
      (goto-char (point-min))
      (search-forward string nil t))))

Empty line at point

(defun my-buffer/empty-line? ()
  (-> (thing-at-point 'line)
      (substring-no-properties)
      (s-trim)
      (equal "")))

Get current line indentation

;;;###autoload
(defun my-buffer/line-indent ()
  "Get the indent of the current line."
  (interactive)
  (or (-some->> (substring-no-properties (thing-at-point 'line))
        (s-match "^\\(\s*\\).*\n$")
        (nth 1)
        (length))
      0))

Check current line for regex

;;;###autoload
(defun my-buffer/line-contains (regexp)
  "Check for REGEXP at current line."
  (save-excursion
    (goto-char (point-at-bol))
    (search-forward-regexp regexp (point-at-eol) t)))

Delete current line

;;;###autoload
(defun my-buffer/delete-current-line ()
  "Delete (not kill) the current line."
  (interactive)
  (save-excursion
    (delete-region
     (progn (forward-visible-line 0) (point))
     (progn (forward-visible-line 1) (point)))))

Insert above/below current line

(defun my-buffer|insert-line-below (&optional str)
  "Insert an empty line below the current line."
  (interactive)
  (save-excursion
    (end-of-line)
    (insert "\n" (or str ""))))

(defun my-buffer|insert-line-above (&optional str)
  "Insert an empty line above the current line."
  (interactive)
  (save-excursion
    (end-of-line 0)
    (insert "\n" (or str ""))))

Map buffer lines

;;;###autoload
(defun my-buffer/map-lines (fun &optional start end)
  "Map lines in buffer with FUN, fn gets called with the line contents."
  (let ((start (or start (point-min)))
        (end (or end (point-max)))
        (lines (list)))
    (save-excursion
      (goto-char start)
      (while (< (point) end)
        (add-to-list 'lines
          (funcall fun (buffer-substring (line-beginning-position) (line-end-position))))
        (forward-line 1))
      (erase-buffer)
      (->> lines
           reverse
           (s-join "\n")
           insert))))

Jump to source

(defun my-buffer|jump-source-dwim (&optional other-window file-name)
  "Jump to the buffer source depending on the mode."
  (interactive)
  (cond
   ((bound-and-true-p org-src-mode) (org-goto-marker-or-bmk org-src--beg-marker))
   (t (dired-jump other-window file-name))))

Search regexp in buffer

(defun my-buffer/search-regexp-in-buffer (regexp)
  "Searches for REGEXP in the current buffer and returns a list of matches and their line numbers."
  (save-excursion
    (let ((matches '()))
      (goto-char (point-min))
      (while (re-search-forward regexp nil t)
        (push (cons (match-string 0) (line-number-at-pos)) matches))
      (reverse matches))))

Find duplicates

(defun my/find-duplicates (s)
  (let* ((words (split-string s "[^A-Za-z]+" t))
         (duplicates (seq-filter
                      (lambda (x) (> (seq-count (lambda (y) (string= x y)) words) 1))
                      (seq-uniq words)))
         (sorted-duplicates (sort duplicates
                                  (lambda (a b)
                                    (> (seq-count (lambda (x) (string= a x)) words)
                                       (seq-count (lambda (x) (string= b x)) words))))))
    sorted-duplicates))

(defun my/find-duplicates-in-region ()
  (unless (region-active-p)
    (user-error "No active region"))
  (let* ((region-start (region-beginning))
         (region-end (region-end))
         (region-text (buffer-substring-no-properties region-start region-end))
         (duplicates (my/find-duplicates region-text)))
    duplicates))

(defun my|evil-ex-replace-duplicates-in-region ()
  (interactive)
  (let ((duplicates (my/find-duplicates-in-region)))
    (when-let ((query (cond
                       ((null duplicates) (prn "No duplicates found"))
                       ((length= duplicates 1) (car duplicates))
                       (t (completing-read "Replace: " duplicates)))))
      (evil-ex (concat "'<,'>s/" query "/")))))

System

Check if clipboard is binary

Get clipboard with (x-get-clipboard)

(defun my/clipboard-binary? ()
  "Check if the current clipboard content is likely to be binary."
  (let ((content (x-get-selection 'CLIPBOARD)))
    (= 1 (string-match-p "[\0-\8\11-\12\14-\31\127-\255]" content))))

Shell Commands & Aliases

Async Command No Window

Prevent async-shell-command displaying a popup or a buffer.

(defun my-shell/async-command-no-window (command)
  "Execute async command without showing the result buffer."
  (interactive)
  (let ((display-buffer-alist (list (cons "\\*Async Shell Command\\*.*" (cons #'display-buffer-no-window nil)))))
    (async-shell-command command)))

Shell command to list

(defun my-shell/command-to-list (cmd)
  "Split output from shell-command to list"
  (split-string (shell-command-to-string cmd) "\n" t))

Start process that stays alive

I don’t want some processes to exit, when I close emacs.

;;;###autoload
(defun my-shell/no-exit-command (cmd &optional &rest args)
  "Launch a shell command, without opening a message buffer.
The proram persists when emacs is closed."
  (let ((args-str (or (-some->> args
                        (s-join " "))
                      "")))
    (call-process-shell-command
       (template "nohup 1>/dev/null 2>/dev/null <<cmd>> <<args-str>> &") nil nil)))

Open youtube link with MPV

;;;###autoload
(defun my-shell/mpv-youtube-url (url)
  (-when-let* ((quality-val
                (-some->> (completing-read
                            "Max height resolution (0 for unlimited): "
                            '("720" "0" "480" "1080"))
                  (string-to-number)))
               (quality-arg (if (> 0 quality-val)
                                (template "--ytdl-format=\"[height<=?<<quality-val>>]\"")
                              "")))
    (message (template "Opening <<url>> at <<quality-val>> with mpv…"))
    (my-shell/no-exit-command "mpv" quality-arg (s-wrap url "\""))))

Shell Command to String

(defun my/shell-command (cmd &rest flags)
  "Execute a shell command and return a cons of the exit-status and the output.
Trims the resulting output automatically."
  (-let (((exit-status . (output))
          (with-temp-buffer
            (list (apply #'call-process (-concat (list cmd nil (current-buffer) nil) (-non-nil flags)))
                  (buffer-string)))))
    (cons exit-status (s-trim output))))

(defun my/shell-command-str (cmd &rest flags)
  "Return the result to a shell command when its exitcode is not stderr."
  (-let (((exit-status . output) (apply #'my/shell-command (-concat (list cmd) (-non-nil flags)))))
    (pcase exit-status
      (0 output))))

(comment
 (equal (my/shell-command "pwd") '(0 . "/home/floscr/.config/doom"))
 (equal (my/shell-command "pwd" nil nil) '(0 . "/home/floscr/.config/doom"))
 (equal (my/shell-command-str "get_url_title" "\"http://stat.us/404\"") nil)
 (equal (my/shell-command-str "get_url_title" "https://orf.at" nil nil) "news.ORF.at"))

Files

Remove trailing slash

(defun my-file/remove-trailing-slash (path)
  "Remove the trailing slash from `path' string."
  (s-replace-regexp "/$" "" path))

(comment
 (my-file/remove-trailing-slash "foo/") ;; "foo"
 nil)

Get file timestamp

;;;###autoload
(defun my-file/timestamp (path)
  (->> (file-attributes path)
       (nth 5)))

Get the last modified file in directory

;;;###autoload
(defun my-file/last-modified-file-in-dir (path)
  (->> (f-entries path)
       (-sort (lambda (a b) (not (time-less-p (my-file/timestamp a)
                                              (my-file/timestamp b)))))
       (car)))

Chmod current file

;;;###autoload
(defun my-file|chmod-this-file ()
  "Chmod +x the current file."
  (interactive)
  (shell-command (template "chmod +x \"<<(buffer-file-name)>>\"")))

Dir has hidden entry

(defun my-file/dir-has-hidden-entries (dir)
  "Check if a DIR has any hidden entries.
Return the first found file when one is found."
  (--find (s-starts-with-p "." (f-filename it)) (f-entries dir)))

Find project root

(defun my-file/project-root (&optional dir)
  "Find the project root either via projectile (not available in certain buffers like dired)
or manually traverse upwards until the .git directory is found."
  (let ((default-directory (or dir default-directory)))
    (or
     projectile-project-root
     (f--traverse-upwards (f-exists? (f-expand ".git" it))))))

(cl-defun my-file/git-root (&optional (skip-worktree? t))
  (let ((checker-fn (if skip-worktree? #'f-dir? #'f-exists?)))
    (f--traverse-upwards (funcall checker-fn (f-expand ".git" it)))))

Find file in project

(defun my-file/find-project-file (path)
  "Find file at relative `path' in the current project."
  (-> (f-join (my-file/project-root) path)
      (find-file)))

Find node_modules executable

(defun my-file/node-modules-executable-find (name)
  "Find an executable bin in the projects node_modules/.bin directory with NAME
Returns nil when no executable was found."
  (-some->> (projectile-project-root)
    (-f-join (f-join "node_modules/.bin" name))
    (-id-when #'f-exists?)))

Debugging / Logging

Kill and Message

;;;###autoload
(defun my/kill-and-message (x)
  "Executes kill-new but with a message log side effect."
  (kill-new x)
  (message "Copied to clipboard: %s" x))

Convert boolean to enabled/disabled string

(defun my/bool-to-enabled-string (x)
  "Convert bool X to string for messaging.
t   -> \"Enabled\")
nil -> \"Disabled\""
  (if x "Enabled" "Disabled"))

Variable t/nil toggle message

(defun my/bool-state-message (x)
  "Log message if a bool is enabled or not"
  (interactive)
  (message (t! "<<(symbol-name x)>>: <<(my/bool-to-enabled-string (symbol-value x))>>")))

Dash Extensions

Fix custom thread indentation

(with-eval-after-load 'dash
  (--each
   (list '-> '->> '-some-> '-some->> '--each '-each '-map '--map)
   (function-put it 'lisp-indent-function nil)))

-tap

Run a side effect fn on the initial input x. But Return the original input x.

;;;###autoload
(defun -tap (fn x)
  "Function docstring"
  (funcall fn x)
  x)

;;;###autoload
(defmacro --tap (fn it)
  "Anaphoric form of `-tap'."
  `(-tap (lambda (it) ,fn) ,it))

-log

Log the current input without breaking the pipe.

;;;###autoload
(defun -log (x)
  "Function docstring"
  (--tap (message "%s" it) x))

-when

;;;###autoload
(defun -when (pred fn x)
  "When FN equals t forward X."
  (if pred
      (funcall fn x)
    x))

;;;###autoload
(defmacro --when (pred form xs)
  "Anaphoric form of -id-when"
  (declare (debug (form form)))
  `(let ((it ,xs))
     (if ,pred
         ,form
       ,xs)))

-id-when

;;;###autoload
(defun -id-when (fn x)
  "When FN equals t forward X."
  (when (funcall fn x) x))

;;;###autoload
(defmacro --id-when (form xs)
  "Anaphoric form of -id-when"
  (declare (debug (form form)))
  `(let ((it ,xs))
     (when ,form ,xs)))

-append

;;;###autoload
(defun -append (elem list)
  "Append ELEM to the end of list.

This is like -snoc but it takes the ELEM as the first argument for easier composition"
  (-snoc list elem))

-shuffle

(defun swap-list-items (LIST el1 el2)
  "in LIST swap indices EL1 and EL2 in place"
  (let ((tmp (elt LIST el1)))
    (setf (elt LIST el1) (elt LIST el2))
    (setf (elt LIST el2) tmp)))

;;;###autoload
(defun -shuffle (LIST)
  "Shuffle the elements in LIST.
shuffling is done in place."
  (loop for i in (reverse (number-sequence 1 (1- (length LIST))))
        do (let ((j (random (+ i 1))))
             (swap-list-items LIST i j)))
  LIST)

-f-join

;;;###autoload
(defun -f-join (x path)
  "Reversed argument order for f-join"
  (f-join path x))

-f-split-ext

;;;###autoload
(defun -f-split-ext (s)
  (list (f-no-ext s) (f-ext s)))

Test

(assert (equal (-f-split-ext "foo_bar.clj") '("foo_bar" "clj")))

-f-map-filename

;;;###autoload
(defun -f-map-filename (fn s)
  (concat (funcall fn (f-no-ext s))
          (-some->> (f-ext s) (concat "."))))

Test

(assert (equal (-f-map-filename #'s-dashed-words "foo_bar.clj") "foo-bar.clj"))
(assert (equal (-f-map-filename #'s-dashed-words "foo_bar") "foo-bar"))

-f-tildify

;;;###autoload
(defun f-tildify (path)
  "Replace the HOME directory in path"
  (s-replace-regexp (t! "^<<(getenv \"HOME\")>>") "~" path))

-mapcar-first

Map over the first item of a list of cons

(defun -mapcar-first (func lst)
  "Apply FUNC to the first item of each cons cell in LST."
  (mapcar (lambda (item)
            (if (consp item)
                (cons (funcall func (car item)) (cdr item))
              item))
          lst))
(mapcar-first #'1+ '((1 . a) (2 . b) (3 . c))))

plist

(defun -plist-get (plist prop)
  (plist-get prop plist))

Tangling

(defvar my-config:literate-config-file
  (concat doom-private-dir "config.org")
  "The file path of your literate config file.")

(defvar my-config:literate-config-file-cache
  (concat doom-cache-dir "literate-last-compile")
  "The file path that `my-config:literate-config-file' will be tangled to, then
byte-compiled from.")

;;;###autoload
(defun my-config/tangle-literate-config (&optional force-p file)
  "Tangles the current buffer FILE if it has changed."
  (let* ((default-directory doom-private-dir)
         (src-file (expand-file-name (or file buffer-file-name)))
         (dst-file (concat (file-name-sans-extension src-file) ".el")))
    (when (or (file-newer-than-file-p src-file
                                      dst-file)
              force-p)
      (message "Compiling your literate config...")
      (start-process
       "org-tangle" nil "emacs"
       "-q" "--batch"
       "-l" "ob-tangle"
       "--eval" (format "(org-babel-tangle-file %S %S)"
                        src-file dst-file)))))

;;;###autoload
(defalias 'my-config/reload-litarate-config-file #'doom/reload)

;;;###autoload
(defun my-config/recompile-literate-config-maybe ()
  "Recompile config.org if we're editing an org file in our DOOMDIR.

We assume any org file in `doom-private-dir' is connected to your literate
config, and should trigger a recompile if changed."
  (when (and (eq major-mode 'org-mode)
             (file-in-directory-p buffer-file-name doom-private-dir))
    (my-config/tangle-literate-config 'force)))

;; Recompile our literate config if we modify it
;;;###autoload
(after! org (add-hook 'after-save-hook #'my-config/recompile-literate-config-maybe))

;;;###autoload
(defun my-config|tangle ()
  "Tangle the current org buffer."
  (interactive)
  (my-config/tangle-literate-config t))

Dates

Insert current date

(defun my-date|insert-today (&optional arg)
  "Insert the current date as YYYY-MM-DD.
When ARG is passed, query for a custom delimiter."
  (interactive "P")
  (let ((delimiter (if arg
                       (read-string "Date delimiter: " "-")
                     "-")))
    (my-evil/insert-dwim (format-time-string (concat "%Y" delimiter "%m" delimiter "%d")))))

(defun my-date|insert-today-mode-specific ()
  (interactive)
  (cl-case major-mode
    ('org-mode (call-interactively #'org-time-stamp-inactive))
    (t (call-interactively #'my-date|insert-today))))

Treepy

treepy-walk-while

(defun +treepy/walk-while (loc pred)
  (require 'treepy)
  (let ((nloc (treepy-next loc)))
    (cond
     ((treepy-end-p nloc) nil)
     ((funcall pred (treepy-node nloc)) nloc)
     (t (treepy-walk-while nloc pred)))))

Clojure like

Porting over clojure-like functions as I’m more used to them

Print

(defun prn (&rest args)
  (let* ((str (->> (-map (lambda (_) "%s") args)
                   (s-join " ")))
         (msg-args (-concat (list str) args)))
    (apply #'message msg-args)))

Doto

(defun doto (x fn)
  (funcall fn x)
  x)

(defun doto-last (fn x)
  (funcall fn x)
  x)

nil?

(defun nil? (x)
  (null x))

(comment
 (nil? nil)
 (nil? t)
 nil)

str

(defun str (&rest args)
  "Cloure like str function"
  (mapconcat (fn!
              (cond
               ((nil? %) "")
               (t (prin1-to-string % t))))
             args ""))

(comment
 (str "foo" 1 "bar" 2 nil 3 :a)
 nil)

inc

(defun inc (n)
  (+ 1 (or n)))

(comment
 (inc nil)
 (inc 0)
 nil)

some?

(defun some? (x)
  (not (nil? x)))

(comment
 (some? 1)
 (some? nil)
 nil)

String

Dedent

(defun my-dedent (s)
  "Dedent a string be the leading space indent."
  (let* ((trimmed (string-trim-left s))
         (trim-count (- (length s) (length trimmed))))
    (->> (s-lines s)
         (--map (s-replace-regexp (concat "^" (s-repeat trim-count " ?")) "" it))
         (s-join "\n"))))

My custom Packages

Bbuild

(defun bbuild|execute (fn cmd args)
  (let* ((default-directory* default-directory)
         (default-directory "/home/floscr/.config/dotfiles/new/modules/scripts")
         (cmd (->> (concat "bb ./src/bbuild.clj"
                           " " cmd " "
                           (-some->> args (-flatten) (s-join " ") (s-prepend " "))
                           (format " --dir \"%s\"" (projectile-project-root default-directory)))
                   (-log))))
    (apply fn (list cmd))))

(defun bbuild|list (&rest args)
  (interactive)
  (ivy-read "Bbuild: " (->> (bbuild|execute #'shell-command-to-string "list" args)
                            ((lambda (x) (split-string x "\n"))))
            :action '(1
                      ("RET"
                       (lambda (&rest args)
                         (bbuild|execute #'compile "execute" args))
                       "Run")
                      ("t"
                       (lambda (&rest args)
                         (bbuild|execute #'call-process-shell-command "execute" (-snoc args "--term")))
                       "In Terminal")
                      ("r" (lambda (x)
                             (-let (((cmd id) x))
                               (bbuild|execute "remove" id)))
                       "Remove"))
            :caller 'bbuild|list))

BBookmarks

Emacs implmenentation for my bookmarks management written in clojure:

https://github.com/floscr/dotfiles/blob/6102660b90048f6be9fbaeb9e710c7de80a85f4c/new/modules/scripts/src/bbookmarks.clj

Docs

Emacs Commands

Command keyDescription
:org-goto "Heading Regexp"Go to an org heading within the file
:org-narrow-indirectNarrow resulting org buffer indirectly

Packages

Package
(package! parseedn)

Config

Disable Emacs bookmarks

(setq bookmark-save-flag nil)

;; Can't set to nil as bookmarks are still set up to hooks
;; Instead I'll keep it in this file which will be trashed on every reboot
(setq bookmark-file "/tmp/emacs-bookmarks-file")

Functions

(defvar-local bbookmarks:project-local? nil)

(defun bbookmarks|execute (&rest args)
  (let ((default-directory "/home/floscr/.config/dotfiles/new/modules/scripts"))
    (->> (concat "bb ./src/bbookmarks.clj"
                 (-some->> args
                           (-filter #'some?)
                           (s-join " ")
                           (s-prepend " ")))
         (shell-command-to-string))))

(defun bbookmarks/goto (x)
  (progn
    (goto-char (point-min))
    (cond
     ((eq 'integer (type-of x))
      (goto-line x))
     ((eq 'string (type-of x))
      (search-forward x nil t)
      (goto-char (line-beginning-position))
      (back-to-indentation)))))

(defun bbookmarks/org-goto (x)
  (when (-some-> (org-find-exact-headline-in-buffer m)
                 (goto-char))
    (goto-char (line-beginning-position))))

(defun bbookmarks/execute-command (cmd)
  (pcase cmd
    (`[:open-file ,f] (progn
                        (when (get-file-buffer f) (setq buffer-open? t))
                        (find-file (f-expand f))
                        (+workspaces-add-current-buffer-h)))

    (`[:goto ,m] (bbookmarks/goto m))

    (`[:goto-bol _ :no-relocate] (unless buffer-open? (bbookmarks/goto m)))
    (`[:goto-bol ,m] (bbookmarks/goto m))

    (`[:org-goto ,m :no-relocate] (unless buffer-open? (bbookmarks/org-goto m)))
    (`[:org-goto ,m] (bbookmarks/org-goto m))
    (`[:org-narrow-indirect] (my-org-indirect|narrow-subtree-indirect :popup? nil))

    (`[:emacs ,cmd] (pcase cmd
                      (:org-capture-goto-last-stored (org-capture-goto-last-stored))
                      (:list-dir (let ((dir default-directory))
                                   (kill-current-buffer)
                                   (counsel-find-file dir)))
                      (_ (user-error (format "No implementation for emacs-command: %s" cmd)))))

    (_ (user-error (format "No implementation for: %s" cmd)))))

(defun bbookmarks/execute (x)
  (require 'parseedn)
  (-let* (((_ _ commands) x)
          (commands (parseedn-read-str commands))
          (buffer-open? nil))
    (-each commands #'bbookmarks/execute-command)))

(defun bbookmarks/remove (x)
  (require 'parseedn)
  (-let* (((_ id _) x))
    (bbookmarks|execute "remove" id)))

(defun bbookmarks|list (&rest args)
  (interactive)
  (ivy-read "Bbookmarks: " (->> (apply #'bbookmarks|execute (-concat '("list" "--with-action") args))
                                ((lambda (x) (split-string x "\n\n")))
                                (--map (split-string it "\n")))
            :action '(1
                      ("RET" bbookmarks/execute "Go to bookmark")
                      ("r" (lambda (x)
                             (-let (((_ id _) x))
                               (bbookmarks|execute "remove" id)
                               (bbookmarks|list)))
                       "Remove"))
            :caller 'bbookmarks|list))

(defun bbookmarks|list-project-bookmarks ()
  (interactive)
  (bbookmarks|list "--parent" (-> (my-file/git-root nil) (my-file/remove-trailing-slash))
                   "--project-root" (-> (my-file/git-root nil) (my-file/remove-trailing-slash))))

(defvar bbookmarks-save-mode-map (make-sparse-keymap))

(define-minor-mode bbookmarks-save-mode
  "Editing indented source code without the indent in an indirect buffer."
  :keymap bbookmarks-save-mode-map)

(cl-defun bbookmarks|save-buffer (&optional parent)
  (interactive)
  (let ((parent (or parent (-> (my-file/git-root nil)
                               (my-file/remove-trailing-slash))))
        (contents (buffer-substring-no-properties (point-min) (point-max))))
    (bbookmarks|execute "add"
                        (shell-quote-argument contents)
                        (when parent "--parent")
                        (when parent (shell-quote-argument parent)))
    (kill-buffer (current-buffer))))

(map! :map bbookmarks-save-mode-map
      :gni "C-c C-c" #'bbookmarks|save-buffer
      :gni "C-c C-k" (cmd! (kill-buffer (current-buffer))))

(add-hook! bbookmarks-save-mode
  (defun bbookmarks-save-mode/init-hook ()
    (setq header-line-format
          "Edit, then exit with 'C-c C-c', abort with 'C-c C-k'.")))

(defun bbookmarks|save ()
  (interactive)
  (let ((file-name (cond
                    ((eq major-mode 'dired-mode) (dired-current-directory))
                    (t (buffer-file-name))))
        (buffer (get-buffer-create "*bbookmark-save*")))
    (if file-name
        (with-current-buffer (get-buffer-create "*bbookmark-save*")
          (insert (format "{:file \"%s\"
 :name \"\"}" file-name))
          (pop-to-buffer buffer)
          (backward-char 2)
          (clojure-mode)
          (bbookmarks-save-mode)
          (evil-insert-state t))
      (user-error "Buffer has no file name!"))))

(defun bbookmarks|save-local ()
  (interactive)
  (call-interactively #'bbookmarks|save)
  (with-current-buffer (get-buffer-create "*bbookmark-save*")
    (setq-local bbookmarks:project-local? t)))

Bindings

(map!
 :leader
 "RET"  #'bbookmarks|list)

(map!
 :leader
 (:prefix-map ("j" . "jumpy")
  :desc "Project"  "p" #'bbookmarks|list-project-bookmarks))

Indirect Indent Mode

Adds minor mode for editing indented source code in an indirect buffer, with the indentation reset to 0. Saving and committing keeps the indentation in the source buffer.

(defvar-local +indirect-indent 0)

(defvar +indirect-indent-mode-map (make-sparse-keymap))

(define-minor-mode +indirect-indent-mode
  "Editing indented source code without the indent in an indirect buffer."
  :keymap +indirect-indent-mode-map)

(map! :map +indirect-indent-mode-map
      :gni "s-s" #'edit-indirect-save)

(add-hook! +indirect-indent-mode
           (defun +indirect-indent/init-hook ()
             (setq header-line-format
                   "Edit, then exit with 'C-c C-c', abort with 'C-c C-k'.")))

(advice-add #'edit-indirect-commit :before #'+indirect-indent/restore-indentation)
(advice-add #'edit-indirect-save :after #'+indirect-indent/remove-indentation)
(advice-add #'edit-indirect-save :before #'+indirect-indent/restore-indentation)

(defun +indirect-indent/restore-indentation ()
  (when (and (bound-and-true-p +indirect-indent-mode)
             (not (eq +indirect-indent 0)))
    (my/without-undo
      (indent-rigidly (point-min) (point-max) (+ +indirect-indent)))))

(defun +indirect-indent/remove-indentation ()
  (when (and (bound-and-true-p +indirect-indent-mode)
             (not (eq +indirect-indent 0)))
    (my/without-undo
      (indent-rigidly (point-min) (point-max) (- +indirect-indent)))))

(defun +indirect-indent|edit (beg end &optional with-mode)
  "Edit script in an indirect buffer."
  (interactive)
  (edit-indirect-region beg end t)
  (let ((indent (indent-rigidly--current-indentation (point-min) (point-max))))
    (unless (eq indent 0)
      (my/without-undo
       (indent-rigidly (point-min) (point-max) (- indent)))
      ;; Local variables get undone when calling a mode
      ;; So we have to define the major mode before
      (funcall with-mode)
      (+indirect-indent-mode 1)
      (setq +indirect-indent indent))))

Evil edit register

Edit registers with +evil-edit-register|counsel. Mostly used to edit the macro registers q.

(defvar +evil-edit-register:register "")
(defvar +evil-edit-register-mode-map (make-sparse-keymap))

(define-minor-mode +evil-edit-register-mode
  "Edit evil register and save it back to the register."
  :keymap +evil-edit-register-mode-map)

(map! :map +evil-edit-register-mode-map
      "C-c C-c" #'+evil-edit-register|save-and-exit
      "C-c C-k" #'kill-buffer-and-window)

(defun +evil-edit-register|save-and-exit (&optional arg)
  "Save the buffer content back to the register register"
  (interactive)
  (evil-set-register
   (string-to-char +evil-edit-register:register)
   (buffer-substring-no-properties (point-min) (point-max)))
  (kill-buffer-and-window))

(defun +evil-edit-register|counsel (register-string)
  "Edit evil register in register"
  (require 'noflet)
  (interactive (noflet ((counsel-evil-registers-action (x) x))
                 (list (counsel-evil-registers))))
  (-when-let* ((register-string (substring-no-properties register-string))
               (buffer (generate-new-buffer (t! "*Evil Register Edit: <<register-string>>*")))
               ((_ reg register) (s-match "^\\[\\(.\\)\\]: \\(.*\\)$" register-string)))
    (pop-to-buffer buffer)
    (with-current-buffer buffer
      (+evil-edit-register-mode 1)
      (setq-local +evil-edit-register:register reg)
      (setq header-line-format "Edit, then exit with 'C-c C-c', abort with 'C-c C-k'.")
      (save-excursion
        (insert register)))))

Scan Management

(defun my-scan|dired-file-document ()
  (interactive)
  (let* ((files (->> (dired-get-marked-files)
                     (-sort #'string<)
                     (-map #'shell-quote-argument)
                     (s-join " ")))
         (bin (f-expand "~/Code/Projects/org_print_scan/result/bin/org_print_scan"))
         (headline (read-string "Headline: "))
         (command (t! "<<bin>> copy <<files>> --headline \"<<headline>>\"")))
    (-log command)
    (shell-command-to-string command)
    (find-file "~/Media/Scans/Scans.org")
    (goto-char (max-char))
    (+org|counsel-org-tag)))

Window Management

Window Listing

(defvar my-wm:window-list-re nil "Regex to parse wmctrl list output.
Example output:
0x014000fb  0 brave-browser.Brave-browser  thinknixx1 The Borrowed by Chan Ho-Kei :: www.goodreads.com/ - Brave")

(setq my-wm:window-list-re
      (rx (seq
           bol
           ;; window id (0x014000fb)
           (submatch "0x" (+ (any digit "a-f"))) "  "
           ;; index
           (submatch (+ (any digit))) " "
           ;; title (brave-browser.Brave-browser)
           (submatch (+ nonl)) "." (submatch (+ anychar))
           ;; right aligned column for username
           " " (+ " ")
           ;; username (thinknixx1)
           (submatch (literal (system-name))) " "
           ;; Window title
           (submatch (* nonl)) eol)))

(defun my-wm/match-wm-string (x)
  (-let* ((xs (-drop 1 (s-match my-wm:window-list-re x)))
          (pid (downcase (nth 0 xs)))
          (monitor (nth 1 xs))
          (process (nth 2 xs))
          (process-title (nth 3 xs))
          (user (nth 4 xs))
          (title (nth 5 xs)))
    (list
     :pid pid
     :monitor monitor
     :process process
     :process-title process-title
     :user user
     :title title)))

(defun my-wm/list-windows ()
  "List X windows"
  (->> (shell-command-to-string "wmctrl -lx")
       (s-split "\n")
       (-drop-last 1)
       (-map #'my-wm/match-wm-string)))

(defun my-wm/-process-eq? (id x)
  (string= id (plist-get x ':process)))

(defun my-wm/-pid-eq? (id x)
  (string= id (plist-get x ':pid)))

Browser Window Listing

(defvar my-wm:browser-title-delimiter-char " ​::​ "
  "The delimiter inserted by the tab title formating extension to seperate the url from the title.
To make sure it is our own delimiter the characters are prefixed and suffixed with zero-width space characters.")

(setq my-wm:browser-url-suffixes '(" - Chromium"
                                    " - Brave"))
(defun my-wm/browser-chop-suffix (url)
  (s-chop-suffixes my-wm:browser-url-suffixes url))

(defun my-wm/is-browser? (x)
  (and (or
        (my-wm/-process-eq? "chromium-browser" x)
        (my-wm/-process-eq? "brave-browser" x))
       (not (s-starts-with? "DevTools - " (plist-get x ':title)))))

(defun my-wm/list-browser-windows ()
  (->> (my-wm/list-windows)
       (-filter #'my-wm/is-browser?)))

(defun my-wm/prev-browser-window ()
  "Try either current or previous window to get the chrome id
Everything further down the line would have to be parsed from bspc history,
and most of the time it's not worth it.
If the current or previous windows are not chrome, just get the first one from the list."
  (--> (my-wm/list-browser-windows)
       (car it)))

(defun my-wm/browser-split-url (x)
  (pcase (s-split my-wm:browser-title-delimiter-char x)
     (`(,title ,url) (list title (my-wm/browser-chop-suffix url)))
     (`(,title) (list title ""))))

(defun my-wm/last-browser-window ()
  (-when-let*
      ((window (my-wm/prev-browser-window))
       ((title url) (-some->> (plist-get window ':title)
                      my-wm/browser-split-url)))
    (list
     title
     (if (s-blank? url) nil url))))

(defun my-wm/last-browser-url ()
  (-let* (((title url) (my-wm/last-browser-window)))
    url))

(defun my-wm/last-browser-url-org-link ()
  (let ((data (my-wm/last-browser-window)))
    (-if-let* (((description link) data))
        (org-make-link-string link description)
      (user-error "Error: Could not get browser url %s" data))))

(defun my-wm/last-browser-url-org-link-formatted ()
  (-if-let* (((title url) (my-wm/last-browser-window))
             (title-formatted
                (cond
                 ;; Custom Pull request Formatting
                  ((s-matches? "^https://github.com.*/pull/[0-9]+.*$" url)
                   (let* ((match (s-match "\\(.+\\) by \\(.+\\) · Pull Request \\(#[0-9]+\\).*$" title))
                          (pr-title (nth 1 match))
                          (pr-user (nth 2 match))
                          (pr-id (nth 3 match)))
                     (template "PR <<pr-id>>: <<pr-title>> by @<<pr-user>>")))
                  (t title))))
      (template "[[<<url>>][<<title-formatted>>]]")
    (user-error "Error: Could not get browser url %s" data)))

Shopping list management

(defun my-org-db/get-db ()
  ""
  (let* (($headlines (->> (org-ml-parse-headlines 'all)))
         (keywords (->> $headlines
                        (--reject (or (eq (org-ml-get-property :level it) 1)
                                      (org-ml-get-children it)
                                      (string= (org-ml-get-property :raw-value (org-ml--get-parent it)) "Incoming")))
                        (--map (org-ml-get-property :raw-value it)))))
    keywords))

(save-window-excursion
  (with-current-buffer (find-file (+org/expand-org-file-name "Db/Shopping.org"))
    (-log (my-org-db/get-db))))

Compile project

My custom solution for caching compile commands between sessions. I store the cached commands per directory in this json file.

(defvar my-project-compile:cache-file nil)
(setq my-project-compile:cache-file (f-join doom-local-dir "project-compile-cache-file.json"))

(comment
 (find-file my-project-compile:cache-file)
 nil)

(defun my-project-compile/update-ht-vector (k v m)
  "Merge the vector on key K in the hashtable M with V.
Removes the key before so the new value will be appended.
This function will mutate the hashtable M."
  (let ((xs (->> (seq-into (or (ht-get m k) []) 'list)
                 (-remove (lambda (x) (string= v x)))
                 (-append v))))
    (ht-set m k (seq-into xs 'vector)))
  m)

(defun my-project-compile/read-cache-file (&optional cache-file)
  (-some->> (or cache-file my-project-compile:cache-file)
    (-id-when #'f-exists?)
    (json-read-file)))

(defun my-project-compile/get-cache-items (key &optional cache-file)
  (or (-some->> (my-project-compile/read-cache-file (or cache-file my-project-compile:cache-file))
        (ht-from-alist)
        ((lambda (m) (ht-get m key))))
      []))

(defun my-project-compile/update-cache (cmd dir &optional cache-file)
  (let ((cache (->> (my-project-compile/read-cache-file)
                    (ht-from-alist)
                    (my-project-compile/update-ht-vector (intern dir) cmd)
                    (json-encode))))
    (f-write cache 'utf-8 (or cache-file my-project-compile:cache-file))))

(defun my-project-compile/compile-cmd (cmd dir &optional cache-file)
  (let ((default-directory dir))
    (call-process-shell-command (format "alacritty --hold -e $SHELL -c '%s'&" cmd) nil 0 nil))
  (my-project-compile/update-cache cmd dir cache-file))

(defun my-project-compile|compile ()
  (interactive)
  (let* ((dir (->> (my-file/project-root)
                   (f-full)))
         (items (->> (my-project-compile/get-cache-items (intern dir))
                     (reverse))))
    (ivy-read "Compile: " (seq-into items 'list)
              :action (lambda (cmd) (my-project-compile/compile-cmd cmd dir)))))

Comment header

;; Variables -------------------------------------------------------------------

(defvar my-comment-header:col-count 80
  "Of how many cols should the header consist.")

(defvar my-comment-header:col-char "-"
  "(Nillable) With which character to fill the comment header.")

(defvar my-comment-header:mode-comment-start
  '((emacs-lisp-mode . ";;")
    (clojurescript-mode . ";;")
    (clojurec-mode . ";;")
    (clojure-mode . ";;")))

(make-variable-buffer-local 'my-comment-header:col-count)
(make-variable-buffer-local 'my-comment-header:col-char)
(make-variable-buffer-local 'my-comment-header:mode-comment-start)

(put 'my-comment-header:col-count 'safe-local-variable 'integerp)
(put 'my-comment-header:mode-comment-start 'safe-local-variable t)

;; Helpers ---------------------------------------------------------------------

(defun my-comment-header/comment-start ()
  "Return the `comment-start' either defined by `my-comment-header/comment-start' or by the `major-mode'."
  (or (alist-get major-mode my-comment-header:mode-comment-start)
      (-> comment-start (s-trim-right))))

(defun my-comment/header-regexp ()
  (concat "^"
          (my-comment-header/comment-start)
          " \\(.+?\\)"
          (when my-comment-header:col-char (concat " " my-comment-header:col-char "+"))
          "$"))

(defun my-comment/match-header-position (&optional bound)
  (re-search-forward (my-comment/header-regexp) bound t))

(defun my-comment/match-header (&optional bound)
  (when (my-comment/match-header-position bound)
    (match-string-no-properties 1)))

(defun my-comment/comment-header-line? ()
  "Is the current line a comment header."
  (save-excursion
    (beginning-of-line)
    (my-comment/match-header (line-end-position))))

(defun my-comment-header/read-string ()
  "Helper function to read the comment header string."
  (read-string (format "Comment header (%s): "
                       (my-comment-header/comment-start))
               (my-comment/comment-header-line?)))

(defun my-comment-header/fill (str)
  "Fill the string with the comment header character."
  (let ((n (- my-comment-header:col-count (length str))))
    (concat str (s-repeat n my-comment-header:col-char))))

(defun my-comment-header|goto-next-comment-header ()
  "Jumps to the next comment header."
  (my-comment/match-header))

(defun my-text/line-empty? ()
  (not (looking-back "^$")))

(defun my-text/ensure-surrounding-newlines ()
  (save-excursion
    (beginning-of-line)
    (previous-line)
    (when (my-text/line-empty?)
      (evil-insert-newline-below)))
  (save-excursion
    (beginning-of-line)
    (next-line)
    (when (my-text/line-empty?)
      (evil-insert-newline-above))))

(defun my-comment-header/buffer-headers ()
  "Get all comment headers in the current buffer."
  (save-excursion
    (goto-char (point-min))
    (let ((items (list)))
      (while-let ((header (my-comment/match-header-position)))
        (add-to-list 'items (cons (match-string-no-properties 1) header))
        (ignore-error (forward-char)))
      (reverse items))))

(defun my-comment-header/goto-headers (header)
  "Go to header with the string value of header"
  (when (-some->> (my-comment-header/buffer-headers)
                  (--find (string= (car it) header))
                  (cdr)
                  (goto-char))
    (beginning-of-line)
    (point)))

;; Main ------------------------------------------------------------------------

(defun my-comment-header|counsel ()
  (interactive)
  (ivy-read "Goto section: "
            (my-comment-header/buffer-headers)
            :action (fn!
                     (goto-char (cdr %))
                     (goto-char (line-beginning-position)))))

(cl-defun my-comment-header/vimish-fold? (&key (pos (point)))
  "Predicate if a fold exist at `pos'."
  (->> (overlays-at pos)
       (some (fn! (->> (vimish-fold--vimish-overlay-p %1)
                       (some (fn! (equal 'vimish-fold--folded %1))))))))

(defun my-comment-header|toggle-fold ()
  (interactive)
  (require 'vimish-fold)
  (when (my-comment/comment-header-line?)
    (if (my-comment-header/vimish-fold?)
        (vimish-fold-unfold)
      (save-excursion
        (vimish-fold-delete)
        (let* ((beg (line-beginning-position))
               (end (or (save-excursion
                          (forward-line 1)
                          (when (my-comment-header|goto-next-comment-header)
                            (progn (previous-line 2)
                                   (point))))
                        (point-max))))
          (vimish-fold beg end))))))

(defun my-comment-header|toggle-fold-or-fallback ()
  (interactive)
  (or (my-comment-header|toggle-fold)
      (call-interactively #'+fold/toggle)))

(defun my-comment-header|fold-all ()
  "Folds everything into comment headers."
  (interactive)
  (vimish-fold-delete-all)
  (save-excursion
    (goto-char (point-min))
    (while (my-comment-header|goto-next-comment-header)
      (my-comment-header|toggle-fold)
      (evil-next-line 1))))

(defun my-comment-header|insert (title)
  "Insert a comment header on the current line."
  (interactive (list (my-comment-header/read-string)))
  (let* ((existing-header (my-comment/comment-header-line?))
         (str-beg (concat (my-comment-header/comment-start) " "))
         (str (->> (concat str-beg title " ")
                   (my-comment-header/fill))))
    (when existing-header
      (delete-line))
    (insert str)
    (goto-char (line-beginning-position))
    (forward-char (length (concat str-beg title)))
    (my-text/ensure-surrounding-newlines)
    (evil-insert-state)
    (overwrite-mode 1)))

(after! vimish-fold
  (setq my-comment-header:col-count vimish-fold-header-width))

Eval Function and last marker

(defun my-eval-function-and-last-marker ()
  "Mark an expression with 'f' and keep working on a function."
  (interactive)
  (cider-eval-defun-at-point)
  (catch 'found
    (save-window-excursion
      (dolist (w (window-list))
        (select-window w)
        (when (member major-mode '(clojure-mode clojurescript-mode clojurec-mode))
          (save-excursion
            (when (evil-goto-mark ?f t)
              (cider-eval-last-sexp)
              (throw 'found t))))))))

Project Hydras

(defun my-project-hydra|main ()
  (interactive)
  (pcase (projectile-project-name)
    ("org-web" (my-project-hydra|org-web/body))
    (_ (user-error "No project hydra found."))))

Hydras

org-web

(defhydra my-project-hydra|org-web (:exit t)
  "org-web Hydra"
  ("r" (lambda () (interactive) (cider-interactive-eval "(biff/fix-print (biff/refresh))")))
  ("tp" (lambda () (interactive) (projectile-run-async-shell-command-in-root "bb org:test")) "Run parser tests")

  ;; File
  ("ff" (lambda () (interactive) (find-file (f-join (projectile-project-root) "projects/backend/resources/fixtures.secret.edn"))) "File: Secret Fixtures")
  ("fr" (lambda () (interactive) (find-file (f-join (projectile-project-root) "projects/backend/src/com/org_web/repl.clj"))) "File: Repl")
  ("fs" (lambda () (interactive) (find-file (f-join (projectile-project-root) "projects/backend/storage/"))) "File: Storage")

  ("xx" (lambda () (interactive) (f-delete (f-join (projectile-project-root) "projects/backend/storage/xtdb") t)) "File: Storage"))

Scratch Popup

(defun +my|scratch-popup ()
  "Open capture, adjust display, disable posframe."
  (interactive)
  (let* ((name "emacs-float-scratch")
         (display ":0")
         (height (* (/ (display-pixel-height display) 4) 2))
         (width (* (/ (display-pixel-width display) 4) 2))
         (frame (make-frame `((name . ,name)
                              (transient . t)
                              (height . (text-pixels . ,height))
                              (width . (text-pixels . ,width))))))
    (select-frame frame)
    ;; HACK: workaround for emacs setting the title correctly and xmonad recognizing it as a window rule
    ;; Generally emacs sets the name way to late for any tiling window manager to recognize it
    (set-frame-name (concat name "1"))
    (set-frame-name name)
    ;; HACK: Emacs sets frame dimensions in number of charters / lines
    ;; This can be worked around with text-pixels, but that would round the next character cell
    ;; So we force the window to the dimensions the tiling wm would have given it
    (set-frame-size (selected-frame) width height t))
  (doom/switch-to-scratch-buffer t)
  (org-mode)
  (spell-fu-mode)
  (evil-insert-state))

Org Web

(defun org-web/execute (&rest args)
  (let ((default-directory "~/Code/Projects/org-web/projects/cli"))
    (->> (concat "bb ./src/cli/core.clj"
                 (-some->> args (s-join " ") (s-prepend " ")))
         (shell-command-to-string))))

(defun org-web/headings (&rest args)
  (ivy-read "Bbookmarks: " (->> (apply #'org-web/execute (-concat '("headings") args))
                                (s-split "\n"))))

(comment
 (org-web/headings "/home/floscr/Documents/Org/Main/programming.org")

 (org-web/headings "/home/floscr/Documents/Org/Main/inbox.org")
 nil)

Gitmoji

Self-written simpler alternative to emacs-gitmoji

Emojis

Penpot

(defvar penpot-gitmoji
  (ht
   (":bug:"
    (ht ('emoji "🐛")
        ('entity "&#x1f41b;")
        ('code ":bug:")
        ('description "A commit that fixes a bug.")
        ('name "bug")
        ('semver nil)))
   (":sparkles:"
    (ht ('emoji "")
        ('entity "&#x2728;")
        ('code ":sparkles:")
        ('description "A commit that an improvement.")
        ('name "sparkles")
        ('semver nil)))
   (":tada:"
    (ht ('emoji "🎉")
        ('entity "&#x1f389;")
        ('code ":tada:")
        ('description "A commit with new feature.")
        ('name "tada")
        ('semver nil)))
   (":recycle:"
    (ht ('emoji "♻️")
        ('entity "&#x2672;")
        ('code ":recycle:")
        ('description "A commit that introduces a refactor.")
        ('name "recycle")
        ('semver nil)))
   (":lipstick:"
    (ht ('emoji "💄")
        ('entity "&#x1f484;")
        ('code ":lipstick:")
        ('description "A commit with cosmetic changes.")
        ('name "lipstick")
        ('semver nil)))
   (":ambulance:"
    (ht ('emoji "🚑")
        ('entity "&#x1f691;")
        ('code ":ambulance:")
        ('description "A commit that fixes critical bug.")
        ('name "ambulance")
        ('semver nil)))
   (":books:"
    (ht ('emoji "📚")
        ('entity "&#x1f4da;")
        ('code ":books:")
        ('description "A commit that improves or adds documentation.")
        ('name "books")
        ('semver nil)))
   (":construction:"
    (ht ('emoji "🚧")
        ('entity "&#x1f6a7;")
        ('code ":construction:")
        ('description "A WIP commit.")
        ('name "construction")
        ('semver nil)))
   (":boom:"
    (ht ('emoji "💥")
        ('entity "&#x1f4a5;")
        ('code ":boom:")
        ('description "A commit with breaking changes.")
        ('name "boom")
        ('semver nil)))
   (":wrench:"
    (ht ('emoji "🔧")
        ('entity "&#x1f527;")
        ('code ":wrench:")
        ('description "A commit for config updates.")
        ('name "wrench")
        ('semver nil)))
   (":zap:"
    (ht ('emoji "")
        ('entity "&#x26a1;")
        ('code ":zap:")
        ('description "A commit with performance improvements.")
        ('name "zap")
        ('semver nil)))
   (":whale:"
    (ht ('emoji "🐳")
        ('entity "&#x1f433;")
        ('code ":whale:")
        ('description "A commit for docker-related stuff.")
        ('name "whale")
        ('semver nil)))
   (":rewind:"
    (ht ('emoji "")
        ('entity "&#x23ea;")
        ('code ":rewind:")
        ('description "A commit that reverts changes.")
        ('name "rewind")
        ('semver nil)))
   (":paperclip:"
    (ht ('emoji "📎")
        ('entity "&#x1f4ce;")
        ('code ":paperclip:")
        ('description "A commit with other not relevant changes.")
        ('name "paperclip")
        ('semver nil)))
   (":arrow_up:"
    (ht ('emoji "⬆️")
        ('entity "&#x2b06;")
        ('code ":arrow_up:")
        ('description "A commit with dependencies updates.")
        ('name "arrow_up")
        ('semver nil)))))

Functions

(defun my-gitmoji/emojis ()
  (-map (fn! (ht-get %1 'emoji)) (ht-values penpot-gitmoji)))

(defun my-gitmoji/emoji-codes ()
  (-map (fn! (ht-get %1 'code)) (ht-values penpot-gitmoji)))

(defun my-gitmoji/gitmoji? (emoji)
  (-contains? (my-gitmoji/emojis) emoji))

(defun my-gitmoji/gitmoji-code? (emoji)
  (-contains? (my-gitmoji/emoji-codes) emoji))

(defun my-gitmoji/gitmoji-code-at-point ()
  (save-excursion
    (goto-char (point-min))
    (when-let* ((to (re-search-forward "\\(:[^[:space:]]+?:\\)" nil t)))
      (buffer-substring-no-properties (point-min) to))))

(defun my-gitmoji/current-gitmoji ()
  "Returns the gitmoji for the current buffer when there is one."
  (ignore-errors
    (let ((char (save-excursion
                  (goto-char (point-min))
                  (evil-find-thing nil 'symbol))))
      (if (my-gitmoji/gitmoji? char)
          char
        (let ((code (my-gitmoji/gitmoji-code-at-point)))
          (when (my-gitmoji/gitmoji-code? ":books:")
            code))))))

(defun my-gitmoji/emoji->emoji-code (emoji)
  (-some-> (ht-find (fn! (string= (ht-get %2 'emoji) emoji)) penpot-gitmoji)
           (-second-item)
           (ht-get 'code)))

(comment
 (my-gitmoji/gitmoji? "x")
 (my-gitmoji/gitmoji? "🧑")
 (my-gitmoji/emoji->emoji-code "")
 nil)

(defun my-gitmoji/prev-commit-gitmoji ()
  (when-let ((emoji (-some-> (magit-rev-format "%B" "HEAD")
                             (substring 0 1))))
    (when (my-gitmoji/gitmoji? emoji)
      emoji)))

(defun my-gitmoji/remove-gitmoji (chars)
  (save-excursion
    (goto-char (point-min))
    (delete-char (length chars))))

(defun my-gitmoji|replace ()
  (interactive)
  (let* ((result (completing-read "Insert gitmoji: "
                                  (ht-map (lambda (_ v)
                                            (concat (ht-get v 'emoji) " (" (ht-get v 'name) ") " (ht-get v 'description)))
                                          penpot-gitmoji)))
         (emoji (s-left 1 result))
         (insert? (my-buffer/empty-line?))
         (current-emoji (my-gitmoji/current-gitmoji))
         (code? (> (length current-emoji) 1)))
    (when current-emoji
      (my-gitmoji/remove-gitmoji current-emoji))
    (save-excursion
      (goto-char (point-min))
      (insert (my-gitmoji/emoji->emoji-code emoji))
      (when (not (equal (string (char-after)) " "))
        (insert " ")))
    (when insert?
      (end-of-line)
      (evil-insert-state))))

Bindings

(map!
 :map git-commit-mode-map
 :leader
 (:prefix-map ("i" . "insert")
  :desc "Gitmoji" "g" #'my-gitmoji|replace))

Default Configuration

Temp

Include colons as delimiters for now

(modify-syntax-entry ?: "w" emacs-lisp-mode-syntax-table)
(after! org-mode
  (modify-syntax-entry ?: "w" org-mode-syntax-table))
(after! grep-mode
  (modify-syntax-entry ?: "w" grep-mode-syntax-table))

Secrets

Config files that I don’t want to share with the world. They will be stored in here:

(defvar my-secrets:config-file nil
  "My private config file.")
(setq my-secrets:config-file "~/.config/secrets.el")

And I will load them on system start:

(defun my-secrets/load-config-file ()
  (-some->> my-secrets:config-file
    (-id-when 'file-exists-p)
    (load-library)))

(my-secrets/load-config-file)

Custom Variables

Directories

Downloads

(defcustom my-directories:downloads-dir "~/Downloads"
  "Downloads Directory"
  :type 'string)

Repositories

(defcustom my-directories:repositories-dir "~/Code/Repositories"
  "Downloads Directory"
  :type 'string)

Emacs

User Name

(setq user-full-name "Florian Schrödl")

Move items to trash on delete

(setq
 trash-directory "~/.Trash/"
 delete-by-moving-to-trash t)

Automatically reload tags files

(setq tags-revert-without-query 1)

Disable blinking cursor

(blink-cursor-mode -1)
(setq blink-matching-paren nil)
(setq visible-cursor nil)

Safe local variables

Variables that I want to safely set from .dir-locals files.

(put '+file-templates-dir 'safe-local-variable #'stringp)

Buffer naming

(defun my-*set-uniquify-buffer-name-style ()
  (setq uniquify-buffer-name-style 'post-forward)
  (setq uniquify-separator "/")
  (setq uniquify-after-kill-buffer-p t)
  (setq uniquify-ignore-buffers-re "^\\*"))

(add-hook! 'persp-mode-hook #'my-*set-uniquify-buffer-name-style)

Load .authinfo.gpg for authentication

(add-to-list 'auth-sources "~/.config/gnupg/.authinfo.gpg")

Disable dcl mode for password files

Since it has the regex matching .com it’s enabled for all my password files, which I name after the matching url.

(setq auto-mode-alist (rassq-delete-all 'dcl-mode auto-mode-alist))

Doom

Init Modules

(doom!
 :completion
 (company
  +childframe)
 (ivy
  +hydra
  +childframe)

 :ui
 doom
 modeline
 doom-quit
 (popup +all +defaults)
 vc-gutter
 vi-tilde-fringe
 window-select
 workspaces
 zen

 :editor
 (format +onsave)
 (evil +everywhere)
 file-templates
 fold
 rotate-text
 multiple-cursors
 (parinfer +rust)
 snippets

 :term
 eshell
 term
 ;; vterm

 :emacs
 (dired +icons)
 electric
 vc
 (undo +tree)

 :checkers
 (syntax +childframe)
 grammar
 spell

 :tools
 direnv
 (lookup
  +devdocs
  +docsets
  +dictionary
  +offline)
 eval
 editorconfig
 (magit +forge)
 rgb
 pdf
 pass
 docker
 lsp
 tree-sitter

 :lang
 beancount
 lua
 (nix +lsp)
 rust
 rest
 data
 (haskell +lsp)
 emacs-lisp
 markdown
 (clojure +lsp)
 json
 (org
  +dragndrop
  +present)
 sh
 yaml
 (web +css)

 :app
 calendar
 (rss +org)

 :config
 (default +bindings +snippets +evil-commands)

 :private
 (org
  +org-reading-list
  +org-tags
  +org-pinboard))

Garbage collection

Set it to 32 MiB.

(setq doom-gc-cons-threshold 33554432)

UI

Functions

Toggle window split style

;;;###autoload
(defun my-ui|toggle-window-split-direction ()
  "Toggle current window split between horizontal and vertical."
  (interactive)
  (if (= (count-windows) 2)
      (let* ((this-win-buffer (window-buffer))
             (next-win-buffer (window-buffer (next-window)))
             (this-win-edges (window-edges (selected-window)))
             (next-win-edges (window-edges (next-window)))
             (this-win-2nd (not (and (<= (car this-win-edges)
                                         (car next-win-edges))
                                     (<= (cadr this-win-edges)
                                         (cadr next-win-edges)))))
             (splitter
              (if (= (car this-win-edges)
                     (car (window-edges (next-window))))
                  'split-window-horizontally
                'split-window-vertically)))
        (delete-other-windows)
        (let ((first-win (selected-window)))
          (funcall splitter)
          (if this-win-2nd (other-window 1))
          (set-window-buffer (selected-window) this-win-buffer)
          (set-window-buffer (next-window) next-win-buffer)
          (select-window first-win)
          (if this-win-2nd (other-window 1))))))

Toggle window dedicated

Lock a window so the buffer can’t be changed or it cant be deleted.

;;;###autoload
(defun my-ui|toggle-window-dedicated ()
  "Control whether or not Emacs is allowed to display another
buffer in current window."
  (interactive)
  (let* ((window (get-buffer-window (current-buffer)))
         (is-locked (window-dedicated-p window))
         (txt (if is-locked "Window unlocked" "Window locked")))
    (set-window-dedicated-p window (not is-locked))
    (message (template "<<(current-buffer)>>: <<txt>>!"))))

Adjust font to display

;;;###autoload
(defun my-ui/adjust-font (size line-space &optional font-family weight)
  (let* ((font-family (or font-family)))
    (setq-default line-spacing line-space)
    (setq-default doom-font (font-spec :family font-family :size size :weight weight))
    (setq-default doom--font-scale nil)
    (set-frame-font doom-font 'keep-size t)
    (doom/reload-font)
    (run-hooks 'after-setting-font-hook)))

;;;###autoload
(defun my-ui/get-x-screen ()
  "Get a list of all connected screens."
  (-> "xrandr | grep ' connected ' | cut -d ' ' -f 1"
      (shell-command-to-string)
      (split-string "\n")
      (reverse)
      (cdr)
      (reverse)
      (cl-sort (lambda (a b)
                 (cond ((string-match-p "^eDP" a) t)
                       ((string-match-p "^eDP" b) nil)
                       (t nil))))))

;;;###autoload
(defun my-ui/has-display-connected? (screen)
  "Check if SCREEN is connected."
  (-contains? (my-ui/get-x-screen) screen))

;;;###autoload
(defun my-ui|adjust-ui-to-display ()
  "Adjust the UI to the current attached display."
  (interactive)
  (cond
   ((string= (system-name) "mbair")
    ;; Not actually a retina display, but this looks best
    (my-ui/adjust-font 13 5 "Fira Code"))
   ((string= (system-name) "Florians-iMac.local")
    (my-ui/adjust-font 14 10 "Menlo"))
   ((string= (system-name) "thinknix")
    (if (my-ui/has-display-connected? "DP2")
        (my-ui/adjust-font 18 7 "Fira Code" 'medium)
      (my-ui/adjust-font 15 4 "Fira Code" 'medium)))
   ((string= (system-name) "thinknixx1")
    (if (my-ui/has-display-connected? "DP-3")
        (my-ui/adjust-font 18 7 "Fira Code" 'medium)
      (my-ui/adjust-font 15 4 "Fira Code" 'medium)))
   ((string= (system-name) "Florians-MacBook-Air.local")
    (my-ui/adjust-font 14 10 "Menlo"))))

Line spacing hydra

Change and reset line-spacing for all buffers.

(defvar my-ui:default-line-spacing line-spacing)
(defvar my-ui:default-line-spacing-increment-step 1)
(defvar my-ui:default-big-line-spacing-increment-step 10)

(defun my-ui/set-line-spacing (&optional increment)
  "Set the line spacing
When no line spacing is given is the default-line-spacing"
  (setq-default line-spacing (+ (or increment my-ui:default-line-spacing-increment-step) line-spacing)))

(defun my-ui|reset-line-spacing ()
  (interactive)
  (setq-default line-spacing my-ui:default-line-spacing))

(defun my-ui|increase-line-spacing ()
  (interactive)
  (my-ui/set-line-spacing))

(defun my-ui|decrease-line-spacing ()
  (interactive)
  (my-ui/set-line-spacing (- my-ui:default-line-spacing-increment-step)))

(defun my-ui|increase-line-spacing-big ()
  (interactive)
  (my-ui/set-line-spacing my-ui:default-big-line-spacing-increment-step))

(defun my-ui|decrease-line-spacing-big ()
  (interactive)
  (my-ui/set-line-spacing (- my-ui:default-big-line-spacing-increment-step)))

(evil-define-key 'normal 'global (kbd "]z") #'my-ui/line-spacing-hydra/body)

;;;###autoload (autoload '+common-lisp/macrostep/body "lang/common-lisp/autoload/hydras" nil nil)
(defhydra my-ui/line-spacing-hydra (:exit nil :hint nil :foreign-keys run :color pink)
  "
Macro Expansion
^^Definitions                           ^^Compiler Notes             ^^Stickers
^^^^^^─────────────────────────────────────────────────────────────────────────────────────
[_r_] Reset
[_]_] Expand
[_[_] Collapse
[_}_] Expand Big
[_{_] Collapse Big
"
  ("r" my-ui|reset-line-spacing)
  ("]" my-ui|increase-line-spacing)
  ("[" my-ui|decrease-line-spacing)
  ("}" my-ui|increase-line-spacing-big)
  ("{" my-ui|decrease-line-spacing-big)
  ("q" nil "cancel" :color blue))

Theme Toggle

Toggle between a light and a dark theme. Bound to SPC t t.

(defun my-ui|toggle-theme ()
  "Toggle between light and dark themes."
  (interactive)
  (-when-let* ((theme (pcase doom-theme
                        (`doom-one 'doom-one-light)
                        (`doom-one-light 'doom-one))))
    (message (t! "Toggling to theme: <<theme>>"))
    (setq doom-theme theme)
    (load-theme theme)))

Configuration

Zen mode & variable pitch fonts

(setq +zen-text-scale 1.5)

(let ((height 140)
      (size 23))
  (setq doom-variable-pitch-font (font-spec :family "IBM Plex Sans" :size size)
        doom-serif-font (font-spec :family "IBM Plex Sans" :size size)))

Add frame padding

(add-hook! '(window-setup-hook after-make-frame-functions)
  (defun my-ui/init-frame-ui (&optional frame)
    (interactive)
    "Re-enable menu-bar-lines in GUI frames."
    (when-let (frame (or frame (selected-frame)))
      (when (display-graphic-p frame)
        (set-frame-parameter frame 'internal-border-width 15)))))

Theme Modifications

You can blend faces like this

`(cider-test-error-face :background ,(ct-mix-opacity (face-attribute 'cider-test-error-face :background) (face-attribute 'default :background) 0.1))
General
(add-hook 'doom-load-theme-hook #'*doom-themes-custom-set-faces)
Function Start
(defun *doom-themes-custom-set-faces ()
  (set-face-attribute 'fringe nil
                      :foreground (face-background 'default)
                      :background (face-background 'default))
  (custom-set-faces!
General
'(whitespace-tab :background nil)
Bookmarks
'(bookmark-face :background nil)
Cider

Remove border

'(cider-result-overlay-face :box nil)
`(cider-test-error-face :background ,(ct-mix-opacity (face-attribute 'cider-test-error-face :background) (face-attribute 'default :background) 0.1))
Dired Output

Remove the rainbow colors from dired.

(when (eq doom-theme `doom-one)
  '(diredfl-read-priv :foreground "#80899E")
  '(diredfl-write-priv :foreground "#80899E")
  '(diredfl-exec-priv :foreground "#80899E")
  '(diredfl-other-priv :foreground "#80899E")

  '(all-the-icons-dired-dir-face :foreground "#80899E")

  '(diredfl-dir-priv :foreground "#282C34")
  '(diredfl-k-modified :foreground "#FF8E90")

  '(diredfl-number :foreground "#80899E")
  '(diredfl-date-time :foreground "#49505F")
  `(diredfl-dir-name :foreground "#2DADF2"))
Mu4E

Switch the highlight.

'(mu4e-highlight-face :inherit mu4e-unread-face)
Org Mode

Remove the ugly grey background

'(org-column :background nil)
Function End
))
Hooks
Diff Highlighting
(add-hook! '(diff-hl-margin-minor-mode-hook)
  (progn
    (set-face-attribute 'smerge-refined-added nil
                        :foreground (doom-blend "#98be65" "#3e493d" 0.15)
                        :background (doom-lighten "#98bb5d" 0.2))
    (set-face-attribute 'smerge-refined-removed nil
                        :foreground (doom-blend "#ff6c6b" "#4f343a" 0.2)
                        :background (doom-lighten "#ff6c6b" 0.1))))

(add-hook! '(magit-status-mode-hook magit-diff-mode-hook)
           (progn
                   (set-face-attribute 'diff-refine-added nil
                                       :foreground (doom-blend "#98be65" "#3e493d" 0.15)
                                       :background (doom-lighten "#98bb5d" 0.2))
                   (set-face-attribute 'diff-refine-removed nil
                                       :foreground (doom-blend "#ff6c6b" "#4f343a" 0.2)
                                       :background (doom-lighten "#ff6c6b" 0.1))))

Adjust UI

Resize the window font size etc according to the system. This will be disabled in terminal mode.

(add-hook! '(doom-init-ui-hook after-make-frame-functions)
  (defun my-ui*after-make-frame (&rest _)
    (when (display-graphic-p) (my-ui|adjust-ui-to-display))))

Scrolloff

Start scrolling X lines before the end of a screen. Disable for certain modes (terminal & ivy) where the window is to small.

(setq scroll-conservatively 10)
(setq scroll-margin 10)

(add-hook 'term-mode-hook (cmd! (setq-local scroll-margin 0)))
(add-hook 'ivy-mode-hook (cmd! (setq-local scroll-margin 0)))

(Visual) Fill Column

(setq-default fill-column 110)
(setq fill-column 110)
(setq visual-fill-column-width fill-column)

(setq visual-fill-column-center-text t
      visual-fill-column-width fill-column)

Disable trailing whitespace warning

(setq-hook! 'prog-mode-hook show-trailing-whitespace nil)

Fix underline

Draw the underline at the bottom of the text, not at the end of line-spacing.

(setq x-underline-at-descent-line nil)

Text

Functions

Expand region hydra

Expand visual region using a hydra. Double press v to enable.

(defhydra my-text/expand-region-hydra ()
   "region: "
   ("f" er/mark-defun "defun")
   ("v" er/expand-region "expand")
   ("V" er/contract-region "contract"))

(evil-define-key 'visual 'global (kbd "v") #'my-text/expand-region-hydra/body)

Unfill Paragraph

Fix a paragraph that was formatted to a fill column.

(defun my-text|unfill-paragraph ()
  "Fix a paragraph that was formatted to a fill column."
  (interactive)
  (let ((fill-column (point-max)))
    (fill-paragraph nil)))

Abbrev spell correction

EmacsWiki: Abbrev Mode Correcting Typos and Misspellings with Abbrev - Mastering Emacs

Config

(define-abbrev-table 'global-abbrev-table
  '(("fnuction" "function" nil 1)
    ("lable" "label" nil 1)))

;; Only enable abbrev in text modes
(add-hook 'text-mode-hook #'abbrev-mode)

Package Configuration

Package Overrides / Disabling / Pinning

Packages that I haven’t yet moved to their structure.

Doom Snippets

(package! doom-snippets :ignore t)
(package! my-doom-snippets
  :recipe (:host github
           :repo "floscr/doom-snippets"
           :files ("*.el" "*")))

Posframe

(package! flycheck-posframe :pin "6eea204138721f511051af5138326fecaad237b7")

Calfw

Continuous events are broken in the current calfw source, also it seems it isn’t maintained anymore. This Commit fixes the behavior.

(package! calfw :recipe (:host github :repo "floscr/emacs-calfw") :pin "e3d04c253230ed0692f161f527d4e42686060f62")
(package! calfw-org :recipe (:host github :repo "floscr/emacs-calfw") :pin "e3d04c253230ed0692f161f527d4e42686060f62")
(package! calfw-ical :pin "e3d04c253230ed0692f161f527d4e42686060f62")
(package! calfw-cal :disable t)
(package! org-gcal :disable t)

json-proces-client

This package is originally hosted on https://gitea.petton.fr/nico/json-process-client.git/ But this private repository host went down a few times, so I’ll stick to github.

(package! json-process-client
  :recipe (:host github :repo "emacsmirror/json-process-client")
  :pin "373b2cc7e3d26dc00594e0b2c1bb66815aad2826")

Remove those annoying LSP interface plugins

(package! lsp-ui :disable t)

merlin

(package! merlin :pin "e4791e22986993c36c3f5c91e8dff93494cc232e")
(package! merlin-eldoc :disable t)

Squlite3

(package! sqlite3)

pcre2el

Fix for missing package

s-replace-all: Symbol’s function definition is void: rxt-quote-pcre

(package! pcre2el)

Emacs

Libraries

Packages that enhance or fix emacs-lisp.

Override functions like variables with using (flet ((#'my-fn)))

Since flet was deprecated, I’m using this for now. Pretty much only used in Expand snippet by name.

Package
(package! noflet)

Doom

Popups

Defaults

These will be written to init.el so it overwrites the doom standard values.

(setq +popup-defaults
  (list :side   'bottom
        :height 0.45
        :width  40
        :quit   t
        :select t
        :ttl    5))

Rules

(set-popup-rules!
  '(("^\\*Org Agenda" :side right :size 0.55 :select t :modeline t :ttl nil :quit nil)
    ("^\\*Org Src" :ignore t)
    ("^\\*Org QL View: \\(Work \\)?Projects*" :side right :size 0.55 :select t :modeline t :ttl nil :quit nil)
    ("^\\*PDF-Occur*" :side right :size 0.5 :select t :modeline t)
    ("^\\*WoMan " :side right :size 0.5 :select t :modeline t :ttl nil :quit nil)
    ("^\\*helm" :vslot -100 :size 0.32 :ttl nil)
    ("^\\*helpful command" :side right :size 0.5 :select t :modeline t :ttl nil :quit nil)
    ("^\\*nodejs" :side right :size 0.55 :select t :modeline t :ttl nil :quit nil)
    ("^\\*projectile-files-errors" :ignore t)
    ("^\\*elfeed-entry" :modeline t :ttl nil)
    ("^\\*Flycheck checker" :size 0.2 :select nil)
    ("^\\*cider-error" :size 0.2 :select nil :ttl t)
    ("^\\*ChatGPT" :ttl nil :quit nil)
    ("^\\*Gptel" :ttl nil :quit nil)))

Themes

Custom doom themes package

Package
(package! doom-themes
  :recipe (:host github :repo "floscr/emacs-doom-themes" :files ("*.el" "themes/*.el"))
  :pin nil)

Workspaces

Functions

Switch to

Enhancement of the default +workspace/switch-to. This allows quick deletion of workspaces from ivy with CTRL + BACKSPACE.

(defvar counsel-workspace-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-<backspace>") #'my-workspaces|switch-to-delete-space)
    map))

(defun my-workspaces/switch-to-delete-space (workspace)
  (let* ((current-workspace-name (+workspace-current-name))
         (new-workspace-name
            (or (--first (string= current-workspace-name it) (+workspace-list-names)) "main")))
    (+workspace/delete workspace)
    (+workspace-switch new-workspace-name)
    (my-workspaces/switch-to)))

(defun my-workspaces|switch-to-delete-space ()
  (interactive)
  (ivy-set-action #'my-workspaces/switch-to-delete-space)
  (ivy-done))

(defun my-workspaces/switch-to ()
  (interactive)
  (ivy-read "Switch to workspace: "
            (+workspace-list-names)
            :keymap counsel-workspace-map
            :action #'+workspace/switch-to))
Save workspace buffers
(defun my-workspaces|save-buffers ()
  "Close all other workspaces."
  (interactive)
  (when-let ((file-buffers (->> (+workspace-buffer-list)
                                (-filter #'buffer-file-name))))
    (-each file-buffers #'basic-save-buffer)))
Close others
(defun my-workspaces|list-unnamed ()
  (->> (+workspace-list-names)
       (--filter (s-matches? "^#[0-9]+$" it))))

(defun my-workspaces|close-others ()
  "Close all other workspaces."
  (interactive)
  (--> (+workspace-list-names)
       (--reject (string= (+workspace-current-name) it) it)
       (-each it #'+workspace-kill)))

(defun my-workspaces|close-unnamed ()
  (interactive)
  (-> (my-workspaces|list-unnamed)
      (-each #'+workspace-kill)))
Find workspace file

Most of the time you create workspaces from a project. But when the CWD has changed in that workspace, you would have to relocate to the projects cwd to find a file.

(defun my-workspaces/workspace-project-root (&optional arg)
  "Gets the root dir for the current workspace"
  (--find (s-match (concat (+workspace-current-name) "/$") it) projectile-known-projects))

(defun my-workspaces|find-workspace-project-file ()
  "Projectile find file for the project named after the current workspace."
  (interactive)
  (cl-letf (((symbol-function 'projectile-project-root) #'my-workspaces/workspace-project-root))
    (projectile-find-file)))

(defun my-workspaces|workspace-project-vc ()
  "Projectile find file for the project named after the current workspace."
  (interactive)
  (let ((default-directory
          (or (my-workspaces/workspace-project-root)
              (my-file/project-root))))
    (magit-status)))
New named workspace
(defun +workspace/new-named ()
  "Create a new named workspace."
  (interactive)
  (let ((name (read-string "New workspace name: ")))
    (if name (+workspace/new name))))
Cleanup
(defun my-workspaces/remove-other-buffers (&optional keep-alive?)
  "Kill or remove all other buffers from current workspace."
  (interactive)
  (--> (+workspace-buffer-list)
       (--reject (eq (current-buffer) it) it)
       (if keep-alive?
           (persp-remove-buffer it)
         (kill-buffer it))))

(defun my-workspaces|hide-other-buffers ()
  "Hide all inactive buffers from the current workspace."
  (interactive)
  (my-workspaces/remove-other-buffers t))

(defun my-workspaces|kill-other-buffers ()
  "Kill all interactive buffers from the current workspace."
  (interactive)
  (my-workspaces/remove-other-buffers))

(defun my-workspaces|hide-non-project-buffers ()
  "Hide all file buffers that don't belong to the project workspace."
  (interactive)
  (let ((project-path (or (expand-file-name (my-workspaces/workspace-project-root))
                          (projectile-project-root))))
    (-some--> (+workspace-buffer-list)
         ;; Dont remove non-remove buffers
         (--filter (buffer-file-name it) it)
         (--reject (s-contains? project-path (buffer-file-name it)) it)
         (--each (persp-remove-buffer it) it))))

Config

Always add buffers to current workspace

Doom per default adds buffers to the current workspace on find-file. I want buffers added whenever I visit a buffer.

(after! persp-mode
  (defun my-workspaces*add-special-buffer ()
    (if-let* ((name (buffer-name))
              (add-buffer? (or (buffer-file-name)
                               (s-matches? "\\*Org Src.*" name)
                               (s-matches? "\\*Code Review" name))))
      (persp-add-buffer (current-buffer) (get-current-persp))))

  (add-hook 'doom-switch-buffer-hook #'my-workspaces*add-special-buffer))

Lookup

(defun format-text-in-mode (text mode)
  "Format the given TEXT using the specified MODE."
  (with-temp-buffer
    (insert text)
    (funcall mode)
    (indent-region (point-min) (point-max))
    (buffer-string)))

(defun my-lsp/describe-thing-at-point ()
  (interactive)
  (let* ((schema nil))
    (save-excursion
      (when-let* ((foo (ignore-errors
                         (+lookup/definition 'symbol))))
        (save-restriction
          (narrow-to-defun)
          (when (search-forward   ([req] (interface req nil))
                                  ":malli/schema"))
          (sp-kill-sexp nil t)))))
  (lsp-describe-thing-at-point)
  (let* ((buffer (get-buffer "*lsp-help*")))
    (with-current-buffer buffer
      (save-excursion
        (goto-char (point-max))
        (read-only-mode -1)
        (insert "\nMalli schema:\n")
        (yank)
        (setq kill-ring (cdr kill-ring))))))

Evil

Packages

Used for evil-little-word.

Package
(package! evil-plugin :recipe (:host github :repo "tarao/evil-plugins"))

Replace the current selection with a register. Press gr with a following motion character like w.

Package
(package! evil-replace-with-register)
Config
(evil-define-operator my-evil-replace-with-register (count beg end type register)
  "Replacing an existing text with the contents of a register"
  :move-point nil
  (interactive "<vc><R><x>")
  (my-parinfer/save-excursion
   "paren"
   (setq count (or count 1))
   (let ((text (if register
                   (evil-get-register register)
                 (current-kill 0))))
     (goto-char beg)
     (if (eq type 'block)
         (evil-apply-on-block
          (lambda (begcol endcol)
            (let ((maxcol (evil-column (line-end-position))))
              (when (< begcol maxcol)
                (setq endcol (min endcol maxcol))
                (let ((beg (evil-move-to-column begcol nil t))
                      (end (evil-move-to-column endcol nil t)))
                  (delete-region beg end)
                  (dotimes (_ count)
                    (insert text))))))
          beg end t)
       (delete-region beg end)
       (dotimes (_ count)
         (insert text))
       (when (and evil-replace-with-register-indent (/= (line-number-at-pos beg) (line-number-at-pos)))
         ;; indent if more then one line was inserted
         (save-excursion
           (evil-indent beg (point))))))))

(use-package! evil-replace-with-register
  :config
  (setq evil-replace-with-register-key (kbd "gr"))
  (define-key evil-normal-state-map
    evil-replace-with-register-key 'my-evil-replace-with-register)
  (define-key evil-visual-state-map
    evil-replace-with-register-key 'my-evil-replace-with-register))
  • “f” - function
  • “c” - single-line comment
  • “C” - multi-line comment
Package
(package! evil-text-objects-javascript :recipe (:host github :repo "urbint/evil-text-objects-javascript"))
Config
Repeat snipe after further key press
(after! evil-snipe
  (setq evil-snipe-repeat-keys t))

Filters out whitespace from the kill ring

Package
(package! clean-kill-ring
  :recipe (:type git
           :host github
           :repo "NicholasBHubbard/clean-kill-ring.el")
  :pin "3338a186329a1bef19e6781aa75befa5a0d031be")
Config
(use-package! clean-kill-ring
  :config (clean-kill-ring-mode 1))

Macros

Save evil excurision

(defmacro save-evil-excursion (&rest body)
  `(let ((previous-evil-state evil-state))
     (unwind-protect
         (progn ,@body)
       (when (eq previous-evil-state 'normal)
         (evil-normal-state)))))

Functions

Evil Insert

(defun my-evil/insert-dwim (str)
  "The evil normal cursor does not visually represent the emacs insert cursor.
So inserting things will be off by one character.
This function will insert how I would expect to insert depending on the mode.
In normal/visual mode -> After the character
In insert mode -> at the character"
  (if (eq evil-state 'insert)
      (insert str)
    (evil-append 1)
    (insert str)
    (evil-change-to-previous-state)
    (unless (string= "" str) (backward-char 1))))

Evil ex search for string

(defun my-evil/ex-search-str (str)
  "Evil Ex search for STR."
  (setq evil-ex-search-pattern
        (let (evil-ex-search-vim-style-regexp)
          (evil-ex-make-search-pattern str))
        evil-ex-search-offset nil
        evil-ex-last-was-search t)
  (unless (equal (car-safe evil-ex-search-history) str)
    (push str evil-ex-search-history))
  (evil-ex-search-next))

Config

Enable fine undo

Whether evil actions like cw are undone in several steps. This is sometimes annoying, as it might need you to press u multiple times. But I prefer the fine grained control, as I’m often staying longer in insert mode, and don’t want one single undo action for the whole “session”.

(setq evil-want-fine-undo t)

Text Editing

Packages

Package
(package! literate-calc-mode)
Config
(use-package! literate-calc-mode
  :commands (literate-calc-mode literate-calc-minor-mode))

Distraction free writing

Config
Custom Design
(defun my-writeroom/setup ()
  (if writeroom-mode
      (my-writeroom/load)
    (my-writeroom/unload)))

(defun my-writeroom/load ()
  (setq-local line-spacing 0.4)
  (setq-local +writeroom:faceremaps
              (list
               (face-remap-add-relative
                'org-link `(:foreground ,(face-attribute 'default :foreground)
                            :underline ,(face-attribute 'default :foreground)
                            :weight normal))
               (face-remap-add-relative
                'default `(:foreground ,(doom-blend
                                         (face-attribute 'default :background)
                                         (face-attribute 'default :foreground)
                                         0.1))))))

(defun my-writeroom/unload ()
  (kill-local-variable 'line-spacing)
  (mapc #'face-remap-remove-relative +writeroom:faceremaps)
  (kill-local-variable '+writeroom:faceremaps))

(add-hook! 'writeroom-mode-hook #'my-writeroom/setup)

Centered buffers, doom does not support this anymore.

(package! visual-fill-column)

When narrowing to region or defun, make it in an indirect other window.

Package
(package! narrow-indirect :recipe (:host github :repo "emacsmirror/narrow-indirect"))
Bindings
(use-package! narrow-indirect
  :init
  (global-set-key (kbd "C-x n n") 'ni-narrow-to-region-indirect-other-window)
  (global-set-key (kbd "C-x n d") 'ni-narrow-to-defun-indirect-other-window))
Functions
Save parinfer excursion
(defmacro my-parinfer/save-excursion (mode &rest body)
  `(let ((prev-mode (symbol-value 'parinfer-rust--mode)))
     (parinfer-rust--switch-mode ,mode)
     ,@body
     (parinfer-rust--switch-mode prev-mode)))
Disable parinfer during multi-edit

Parinfer doesn’t really work well with multi-cursor-edit, as some braces get auto-cleaned and now the positions are off. So we disable it during a multi edit.

(defvar my-evil:multi-edit-prev-parinfer-mode nil
  "TODO")

(make-variable-buffer-local 'my-evil:multi-edit-prev-parinfer-mode)

(defun my-evil/multi-edit-insert-disable-parinfer ()
  (when (and parinfer-rust-mode (boundp 'evil-multiedit--pt-beg) evil-multiedit--pt-beg)
    (setq-local my-evil:multi-edit-prev-parinfer-mode (symbol-value 'parinfer-rust--mode))
    (parinfer-rust-mode-disable)))

(defun my-evil/multi-edit-insert-re-enable-parinfer ()
  (when my-evil:multi-edit-prev-parinfer-mode
    (parinfer-rust-mode 1)
    (parinfer-rust--switch-mode my-evil:multi-edit-prev-parinfer-mode))
  (setq-local my-evil:multi-edit-prev-parinfer-mode nil))

(add-hook! 'evil-insert-state-entry-hook #'my-evil/multi-edit-insert-disable-parinfer)
(add-hook! 'evil-insert-state-exit-hook #'my-evil/multi-edit-insert-re-enable-parinfer)
Bindings
Paste in paren mode

Better pasting for lisps, automatically indents and fixes parens when pasting.

(defun my-parinfer|yank ()
  "Yank with temporary paren mode enabled."
  (interactive)
  (my-parinfer/save-excursion
   "paren"
   (let ((beg (point)))
     (->> (current-kill 0)
          (substring-no-properties)
          (my-dedent)
          (insert))
     (let ((end (point)))
       (evil-indent beg end)))))

(map!
 :mode (clojure-script-mode clojure-mode emacs-lisp-mode)
 :gi
 "s-v" #'my-parinfer|yank)

format-all

;; Set formatter to empty list which should be defined per mode
(setq +format-on-save-enabled-modes '())

undo-tree

Config
(use-package! undo-tree
  :config
  (setq undo-tree-history-directory-alist `(("." . ,(concat doom-cache-dir "undo-tree-hist/"))
                                            ;; Dont save undo history for org files, they get too big
                                            ("\\.org\\'" . nil))))

smartparens-mode

Functions
yank-sexp
(defun my-sp/yank-sexp (&optional ARG)
  "Yank the current sexp without killing it."
  (interactive)
  (sp-kill-sexp ARG t))

aphelia-mode

Functions
Remove formatter
(defun my-formatters/remove-formatter (key)
  "Remove a formatter set by doom emacs using `set-formatter!'.
Use this function in a hook like this:

(after! (:and clojure-mode apheleia)
  (my-formatters/remove-formatter 'cljfmt))"
  (setq apheleia-formatters (delq (assoc key apheleia-formatters) apheleia-formatters))
  (setq apheleia-mode-alist (seq-remove (lambda (pair) (eq (cdr pair) key)) apheleia-mode-alist)))

(comment
 (progn
   (set-formatter! 'cljfmt '("cljfmt" "fix" "-") :modes '(clojure-mode clojurec-mode clojurescript-mode))
   (my-formatters/remove-formatter 'cljfmt)
   (null (seq-some (lambda (pair) (eq (cdr pair) 'cljfmt)) apheleia-mode-alist)))
 nil)
Config
Remove cljfmt
(after! (:and clojure-mode apheleia)
  (my-formatters/remove-formatter 'cljfmt))

(after! (:and beancount-mode apheleia)
  (my-formatters/remove-formatter 'bean-format))

UI

Higlight indent guides

Package

(package! highlight-indent-guides :pin "cf352c85cd15dd18aa096ba9d9ab9b7ab493e8f6")

Config

(use-package! highlight-indent-guides
  :hook ((stylus-mode) . highlight-indent-guides-mode)
  :init
  (setq highlight-indent-guides-method 'character
        highlight-indent-guides-suppress-auto-error t)
  :config
  (defun +indent-guides-init-faces-h (&rest _)
    (when (display-graphic-p)
      (highlight-indent-guides-auto-set-faces)))

  ;; HACK `highlight-indent-guides' calculates its faces from the current theme,
  ;;      but is unable to do so properly in terminal Emacs, where it only has
  ;;      access to 256 colors. So if the user uses a daemon we must wait for
  ;;      the first graphical frame to be available to do.
  (add-hook 'doom-load-theme-hook #'+indent-guides-init-faces-h)
  (when doom-theme
    (+indent-guides-init-faces-h)))

Programming

General Purpose

Package
(package! tree-sitter)
(package! tree-sitter-langs)
Config
(use-package! tree-sitter
  :config
  (require 'tree-sitter-langs)
  ;; (global-tree-sitter-mode)
  ;; (add-to-list 'tree-sitter-major-mode-language-alist '(typescript-tsx-mode . tsx))
  ;; (add-hook 'tree-sitter-after-on-hook #'tree-sitter-hl-mode))

Community collection of snippets for command line tools.

Package
(package! cheat-sh)
Config
(use-package cheat-sh
  :commands (cheat-sh)
  :config
  (setq cheat-sh-topic-mode-map
        '((awk-mode . "awk")
          (c++-mode . "cpp")
          (c-mode . "c")
          (clojure-mode . "clojure")
          (clojurescript-mode . "clojure")
          (dockerfile-mode . "docker")
          (emacs-lisp-mode . "elisp")
          (fish-mode . "fish")
          (go-mode . "go")
          (haskell-mode . "haskell")
          (hy-mode . "hy")
          (java-mode . "java")
          (js-jsx-mode . "javascript")
          (js-mode . "javascript")
          (js-mode . "js")
          (lisp-interaction-mode . "elisp")
          (lisp-mode . "lisp")
          (objc-mode . "objectivec")
          (pike-mode . "pike")
          (powershell-mode . "powershell")
          (python-mode . "python")
          (rust-mode . "rust")
          (sh-mode . "bash"))))
Package
(package! emmet-mode)
Functions
Indent or Yase Emmet Expand

Source: Doom: modules/lang/web/autoload/html.el

(defun my:emmet/indent-or-yas-or-emmet-expand ()
  "Do-what-I-mean on TAB.

Invokes `indent-for-tab-command' if at or before text bol, `yas-expand' if on a
snippet, or `emmet-expand-yas'/`emmet-expand-line', depending on whether
`yas-minor-mode' is enabled or not."
  (interactive)
  (let ((action (cond ((or (<= (current-column) (current-indentation)
                                    (not (eolp))
                                    (not (or (memq (char-after) (list ?\n ?\s ?\t))
                                             (eobp)))
                                #'indent-for-tab-command))
                       ((featurep! :editor snippets)
                        (require 'yasnippet)
                        (if (yas--templates-for-key-at-point
                                #'yas-expand
                                #'emmet-expand-yas))))
                      (#'emmet-expand-line))))
    (call-interactively action)))
Config
(use-package! emmet-mode
  :preface (defvar emmet-mode-keymap (make-sparse-keymap))
  :hook (css-mode web-mode html-mode haml-mode nxml-mode)
  :config
  (when (require 'yasnippet nil t)
    (add-hook 'emmet-mode-hook #'yas-minor-mode-on))

  (setq emmet-css-major-modes '(css-mode
                                scss-mode
                                stylus-mode
                                sass-mode
                                less-mode
                                less-css-mode))

  (add-hook! 'stylus-mode-hook #'emmet-mode)
  (setq-hook! 'stylus-mode-hook emmet-use-sass-syntax t)
  (setq emmet-indent-after-insert nil)

  (defadvice! my-emmet/fix-stylus-colon (&optional arg1)
     :after '(emmet-expand-yas emmet-expand-line)
     (when (eq major-mode 'stylus-mode)
       (save-excursion
        (search-backward ":" nil nil)
        (delete-char 1))))

  (setq emmet-move-cursor-between-quotes t)
  (map! :map emmet-mode-keymap
        :v [tab] #'emmet-wrap-with-markup
        [tab] #'my:emmet/indent-or-yas-or-emmet-expand
        "M-E" #'emmet-expand-line)

  (puthash "aic" "align-items:center;" emmet-css-snippets)
  (puthash "brd" "border-radius:|;" emmet-css-snippets)
  (puthash "df" "display:flex;" emmet-css-snippets)
  (puthash "dn" "display:none;" emmet-css-snippets)
  (puthash "fdc" "flex-direction:column;" emmet-css-snippets)
  (puthash "fdr" "flex-direction:row;" emmet-css-snippets)
  (puthash "g" "gap:|;" emmet-css-snippets)
  (puthash "jcc" "justify-content:center;" emmet-css-snippets)
  (puthash "ov" "overflow:hidden;" emmet-css-snippets)
  (puthash "pen" "pointer-events:none;" emmet-css-snippets)
  (puthash "po" "top: 0;\nleft: 0;\nright: 0;\nbottom: 0;" emmet-css-snippets)
  (puthash "posa" "position:absolute;" emmet-css-snippets)
  (puthash "posf" "position:fixed;" emmet-css-snippets)
  (puthash "posr" "position:relative;" emmet-css-snippets))

Pretty print colors string with the color as the background.

Package
(package! rainbow-mode)
Config
(use-package rainbow-mode
  :commands (rainbow-mode)
  :config
  ;; Always enable rgb, etc colors
  (setq rainbow-html-colors t))

A live reload server directly in emacs for quickly editing css. Launch using my-impatient-mode|launch.

Package
(package! impatient-mode)
Functions
Launch
(defun my-impatient-mode|launch ()
  "Launch server with impatient mode and open the browser."
  (interactive)
  (httpd-start)
  (impatient-mode)
  (let ((file-name (f-filename (+yank/buffer-filename))))
    (browse-url (t! "http://localhost:8080/imp/live/<<file-name>>"))))

(defun my-impatient-mode|stop ()
  "Launch server with impatient mode and open the browser."
  (interactive)
  (httpd-stop)
  (impatient-mode -1))
Config
(use-package! impatient-mode
  :commands impatient-mode)
  • Select a region and run the command diff-lisp-mark-selected-text-as-a.
  • Select another region and run diff-lisp-diff-a-and-b.
  • The difference of two region is displayed in a buffer.
Package
(package! diff-lisp :recipe (:host github :repo "redguardtoo/diff-lisp"))
Config
(use-package! diff-lisp)

Database

Packages

Emacs database layer interface

(package! edbi)
Bridge

Add nix-shell headers to the bridge

Use with header :tangle "edbi-bridge.pl"

Tangle this file manually and change permissions with

chmod +x ./edbi-bridge.pl
#!/usr/bin/env nix-shell
#! nix-shell -i perl -p mysql -p perlPackages.DBDmysql perlPackages.RPCEPCService perlPackages.DBDPg perlPackages.DBI

use strict;
use RPC::EPC::Service;
use Data::Dumper;
use DBI;

our $dbh = undef;
our $sth = undef;

sub edbi_connect {
  my ($args) = @_;
  my ($data_source,$username,$auth) = @$args;

  # No input _probably_ means "no password" rather than empty string
  $auth = undef if($auth eq "");

  if ($dbh) {
    eval {
      $dbh->disconnect();
    }
  }

  our $dbh = DBI->connect($data_source, $username, $auth)
      or die("Could not connect to database:
Data Source ($data_source)
User Name: ($username):
DBI error: ($DBI::errstr)
");

  return $dbh->get_info(18);
}

sub edbi_do {
  return undef unless $dbh;
  my ($args) = @_;
  my ($sql, $params) = @$args;
  my $rows = $dbh->do($sql, undef, @$params);
  return $rows;
}

sub edbi_select_all {
  return undef unless $dbh;
  my ($args) = @_;
  my ($sql, $params) = @$args;
  my $rows = $dbh->selectall_arrayref($sql, undef, @$params);  return $rows;
}

sub edbi_prepare {
  return undef unless $dbh;
  $sth->finish() if $sth;
  my ($sql) = @_;
  our $sth = $dbh->prepare($sql) or return undef;
  return 'sth';
}

sub edbi_execute {
  return undef unless $sth;
  my ($params) = @_;
  return $sth->execute(@$params) or return undef;
}

sub edbi_fetch_columns {
  return undef unless $sth;
  return $sth->{NAME};
}

sub edbi_fetch {
  return undef unless $sth;
  my ($num) = @_;
  if ($num eq undef) {
    return $sth->fetchall_arrayref();
  } else {
    my $ret = [];
    for (my $i = 0; $i < $num; $i++) {
      my $row = $sth->fetchrow_arrayref();
      last if $row eq undef;
      push @$ret, [@$row];
    }
    return $ret;
  }
}

sub edbi_auto_commit {
  return undef unless $dbh;
  my ($flag) = @_;
  if ($flag eq "true") {
    $dbh->{AutoCommit} = 1;
    return 1;
  } else {
    $dbh->{AutoCommit} = 0;
    return 0;
  }
}

sub edbi_commit {
  return undef unless $dbh;
  $dbh->commit();
  return 1;
}


sub edbi_rollback {
  return undef unless $dbh;
  $dbh->rollback();
  return 1;
}

sub edbi_disconnect {
  return undef unless $dbh;
  $dbh->disconnect();
  return 1;
}

sub edbi_status {
  return undef unless $dbh;
  return [$dbh->err, $dbh->errstr, $dbh->state];
}

sub edbi_type_info_all {
  return undef unless $dbh;
  my $ret = $dbh->type_info_all;
  print STDERR Dumper $ret;
  return $dbh->type_info_all;
}

sub edbi_table_info {
  return undef unless $dbh;
  eval {
    $sth->finish() if $sth;
  };
  my ($args) = @_;
  my ($catalog, $schema, $table, $type) = @$args;
  $sth = $dbh->table_info( $catalog, $schema, $table, $type );
  return [$sth->{NAME}, $sth->fetchall_arrayref()];
}

sub edbi_column_info {
  return undef unless $dbh;
  eval {
    $sth->finish() if $sth;
  };
  my ($args) = @_;
  my ($catalog, $schema, $table, $column) = @$args;
  $sth = $dbh->column_info( $catalog, $schema, $table, $column );
  return [[],[]] unless $sth;
  return [$sth->{NAME}, $sth->fetchall_arrayref()];
}

sub edbi_primary_key_info {
  return undef unless $dbh;
  eval {
    $sth->finish() if $sth;
  };
  my ($args) = @_;
  my ($catalog, $schema, $table) = @$args;
  $sth = $dbh->primary_key_info( $catalog, $schema, $table );
  return undef unless $sth;
  return [$sth->{NAME}, $sth->fetchall_arrayref()];
}

sub edbi_foreign_key_info {
  return undef unless $dbh;
  eval {
    $sth->finish() if $sth;
  };
  my ($args) = @_;
  my ($pkcatalog, $pkschema, $pktable, $fkcatalog, $fkschema, $fktable) = @$args;
  $sth = $dbh->foreign_key_info( $pkcatalog, $pkschema, $pktable,
                                 $fkcatalog, $fkschema, $fktable );
  return undef unless $sth;
  return [$sth->{NAME}, $sth->fetchall_arrayref()];
}

sub main {
  my $methods =
    {
     'connect' => [\&edbi_connect,"data_source, username, auth", ""],
     'do' => [\&edbi_do, "sql, params", ""],
     'select-all' => [\&edbi_select_all, "sql, params", ""],
     'prepare' => [\&edbi_prepare, "sql", ""],
     'execute' => [\&edbi_execute, "params", ""],
     'fetch-columns' => [\&edbi_fetch_columns, "", ""],
     'fetch' => [\&edbi_fetch, "[number]", ""],
     'auto-commit' => [\&edbi_auto_commit, "false/true", ""],
     'commit' => [\&edbi_commit, "", ""],
     'rollback' => [\&edbi_rollback, "", ""],
     'disconnect' => [\&edbi_disconnect, "", ""],
     'status' => [\&edbi_status, "", ""],
     'type-info-all' => [\&edbi_type_info_all, "", ""],
     'table-info' => [\&edbi_table_info, "catalog, schema, table, type", ""],
     'column-info' => [\&edbi_column_info, "catalog, schema, table, column", ""],
     'primary-key-info' => [\&edbi_primary_key_info, "catalog, schema, table", ""],
     'foreign-key-info' => [\&edbi_foreign_key_info, "pk_catalog, pk_schema, pk_table, fk_catalog, fk_schema, fk_table", ""],
    };
    my $server = RPC::EPC::Service->new(0, $methods);
    $server->start;
}

main;
(after! edbi
  (setq edbi:driver-info
        (list
         "nix-shell" "-p"
         "perl"
         "-p" "perlPackages.DBI"
         "-p" "perlPackages.RPCEPCService"
         "-p" "perlPackages.DBDPg"
         "-p" "perlPackages.DBDmysql"
         "--run"
         (f-join doom-private-dir "edbi-bridge.pl"))))

Config

Connections
(defvar my-edbi:connections
  '("dbi:Pg:dbname=penpot;host=localhost"))

(defun my-edbi/extract-db-string (str)
  (if (string-match "dbname=\\([^;]*\\);" str)
      (match-string 1 str)))

(comment
 (my-edbi/extract-db-string (car my-edbi:connections)))

(defun my-edbi|connect ()
  (interactive)
  (let* ((db (completing-read "Database: " my-edbi:connections))
         (conn (edbi:start))
         (table (my-edbi/extract-db-string db)))
    (edbi:connect conn (list db table table))
    (edbi:dbview-open conn)))

Javascript

Packages

RJSX & Typescript
(package! rjsx-mode :pin "b697fe4d92cc84fa99a7bcb476f815935ea0d919")
(package! typescript-mode :pin "dd10f702d4946ddb2e1c2863bf02bf241bce5fc8")

;; TSX Mode
(package! coverlay)
(package! css-in-js-mode :recipe (:host github :repo "orzechowskid/tree-sitter-css-in-js"))
(package! origami)
(package! tsx-mode :recipe (:host github :repo "orzechowskid/tsx-mode.el"))

A javascript auto-fixer that isn’t slow. Needs eslint_d binary in $PATH.

Package
(package! eslintd-fix)
Config
(use-package! eslintd-fix
  :after js2-mode
  :config
  (setq
   flycheck-javascript-eslint-executable (executable-find "eslint_d")
   flycheck-disabled-checkers '(javascript-jshint javascript)))
Functions
Enable eslintd for writable buffers
(defun my:js/start-eslint ()
  "Start eslint on writable buffers.

When enabling on read-only buffers it will throw an error."
  (unless buffer-read-only
    (eslintd-fix-mode)))

(add-hook 'js2-mode-hook #'my:js/start-eslint)
Package
(package! js-import :recipe (:host github :repo "floscr/js-import"))
Config
(put 'js-import-alias-map 'safe-local-variable (lambda (x) t))
Package
(package! jest :recipe (:host github :repo "floscr/emacs-jest"))
Config
(use-package! jest
  :after (js2-mode)
  :hook (js2-mode . jest-minor-mode)
  :config
  (set-popup-rule! "^\\*jest\\*"
    :side 'right
    :size 0.5
    :select nil :quit 'current :ttl nil))

A JavaScript development environment for Emacs.

Package
(package! indium)
Config
(use-package! indium
  :commands (indium-connect indium-launch)
  :init
  (setq indium-chrome-use-temporary-profile t)
  (setq indium-chrome--default-data-dir (f-join (getenv "XDG_CACHE_HOME") "inidum-chrome-profile"))
  (setq indium-chrome-data-dir (f-join (getenv "XDG_CACHE_HOME") "inidum-chrome-profile"))
  (map! :map (js2-mode-map rjsx-mode-map)
        :localleader
        (:prefix ("i" . "Indium")
          :desc "Console"                   "c" #'indium-switch-to-repl-buffer
          :desc "Launch"                    "l" #'indium-launch
          :desc "Launch"                    "q" #'indium-quit
          :desc "Add breakpoint"            "r" #'indium-reload
          (:prefix ("b" . "breakpoint")
            :desc "Add"                     "b" #'indium-add-breakpoint
            :desc "Conditional"             "c" #'indium-add-conditional-breakpoint
            :desc "Conditional"             "e" #'indium-edit-breakpoint-condition
            :desc "Conditional"             "l" #'indium-list-breakpoints
            :desc "Conditional"             "0" #'indium-deactivate-breakpoints
            :desc "Conditional"             "1" #'indium-activate-breakpoints
            :desc "Delete"                  "d" #'indium-remove-breakpoint
            :desc "Delete all from buffer"  "D" #'indium-remove-all-breakpoints-from-buffer
            :desc "Edit Condition"          "e" #'indium-toggle-breakpoint
            :desc "Toggle"                  "t" #'indium-toggle-breakpoint)))
  (map!
        :map indium-inspector-mode-map
        :n "-" #'indium-inspector-pop)
  (map! :map indium-debugger-mode-map
        :n "gr" #'indium-debugger-resume
        :n "gi" #'indium-debugger-step-into
        :n "go" #'indium-debugger-step-over
        :n "ge" #'indium-debugger-evaluate
        :n "gl" #'indium-debugger-locals)
  :config
  (set-popup-rule! "^\\*JS REPL*" :size 0.3)
  (set-popup-rule! "^\\*JS Debugger Locals*" :size 0.3))
Functions
Add CI command

No namespace here, so it matches the other commmands

(defun npm-mode-npm-ci ()
  "Run the 'npm install' command."
  (interactive)
  (npm-mode--exec-process "npm ci"))
Package
(package! graphql)

Functions

Match const function name
(defun my-javascript/match-const-function-name (line)
  "Matches a line to the word after the declaration"
  (nth 2 (s-match
          "\\(const\\|let\\|class\\)\s\\(.+?\\)\s"
          line)))

(defun my-javascript/const-function-at-point ()
  "Returns the current function name at the current line"
  (my-javascript/match-const-function-name (thing-at-point 'line t)))
Setup test files
Rotate test words
(add-hook! '(js2-mode-hook rjsx-mode-hook)
  (defun my-javascript/setup-test-file-vars ()
    "Sets up variables for test files (cypress, jest)."
    (when (-some->> (buffer-file-name)
            (string-match "\\.\\(test\\|spec\\|cy\\)\\.js$"))
      ;; Rotate it/it.only
      (add-to-list 'rotate-text-local-patterns '("it\\(\\.only\\)?" (lambda (x y) (if (string= x "it") "it.only" "it")))))))

Emacs Lisp (Elisp)

Packages

Functions

(defun my-elisp|eval-last-sexp (&optional output-to-current-buffer)
  (interactive "P")
  (let ((previous-evil-state evil-state))
    (save-excursion
      (my-cider/eval-jump-opening-brace)
      (when (eq previous-evil-state 'normal)
        (evil-insert-state)
        (forward-char 1))
      (eval-last-sexp output-to-current-buffer)
      ;; Dont restore state when doing eval when inserting result
      ;; The `cider-eval-print-handler' function is async,
      (when (and (not output-to-current-buffer)
                 (eq previous-evil-state 'normal))
        (evil-normal-state)))))

Config

Remove Rainbow Delimiters

I don’t need them with parinfer and lispyville, they’re just distracting.

(remove-hook 'emacs-lisp-mode-hook #'rainbow-delimiters-mode)

Bindings

(map! :map emacs-lisp-mode-map
      :desc "Eval" :n "RET" #'my-elisp|eval-last-sexp)

Json

Functions

Is Last JSON key at point
(defun my-json/is-last-key? ()
  "Is the next line the last json key."
  (save-excursion
    (forward-line)
    (my-buffer/line-contains "}")))
Insert JSON Key

Insert JSON key in a JSON document This functions is dependant on the yasnippet: key

(defun my-json/insert-key (&optional above?)
  "Adds a new JSON key pair."
  (let ((last-line? (my-buffer/line-contains ",$")))
    ;; Insert comma
    (if (and (not last-line?) (not above?))
        (replace-regexp "$" "," nil (point-at-bol) (point-at-eol)))
    (end-of-line)
    (if above?
        (evil-insert-newline-above)
      (evil-insert-newline-below))
    (indent-according-to-mode)
    (my-snippets/insert-by-name "key")))

(defun my-json|insert-key-above ()
  "Function docstring"
  (interactive)
  (my-json/insert-key t))

(defun my-json|insert-key-below ()
  "Function docstring"
  (interactive)
  (my-json/insert-key nil))
Autofix JSON

Uses json-fix to autofix JSON files.

npm i -g json-fix
(defun my-json|autofix-buffer ()
  "Autofix json buffer"
  (interactive)
  (let ((b (if mark-active (min (point) (mark)) (point-min)))
        (e (if mark-active (max (point) (mark)) (point-max))))
    (shell-command-on-region b e
     (template "json-fix --no-sort --spaces <<tab-width>>") (current-buffer) t)))

Bindings

(map!
 :after json-mode
 :map json-mode-map
 :gni "<C-return>" #'my-json|insert-key-below
 :gni "<C-S-return>" #'my-json|insert-key-above)
Functions
Export default variable
(defun js2r-export-default ()
  "Exports the current declaration at the end of the file"
  (interactive)
  (save-excursion
    (let* ((name (my-javascript/const-function-at-point)))
      (goto-char (point-max))
      (insert "\n")
      (insert (template "export default <<name>>;")))))
Extract constant to file

Extract the const under the cursor into a new file.

(defun js2r-extract-const-to-file ()
  "Extracts function to external file"
  (interactive)
  (let* ((name (my-javascript/const-function-at-point))
         (path (concat "./" name ".js")))
    (evil-digit-argument-or-evil-beginning-of-line)
    (js2r-kill)
    (f-write-text "" 'utf-8 path)
    (find-file path)
    (yank)))
Generate index.js file index

Generate a file index in the current file for every other file in the current directory.

(defun +js/index-file-names (&optional dir)
  "Get filenames from current buffers directory."
  (let ((fs (directory-files (or dir default-directory) nil ".*\\.js")))
    (mapcar 'f-no-ext
            (remove "index.js" fs))))

(defun +js|generate-index (&optional dir ignore-list is-old-es-style)
  "Generate an index import file for files in directory.
Pass DIR for directory, falls back to default-directory
Pass IGNORE-LIST for a list of files
Pass IS-OLD-ES-STYLE for using the non-esm style exports"
  (interactive)
  (erase-buffer)
  (let* ((dir (or dir default-directory))
         (fs (->>
              (+js/index-file-names dir)
              (-log)
              (--reject (s-ends-with-p ".test" it))
              (--when ignore-list (--remove (-contains? ignore-list it) it)))))
    (message "%s" fs)
    (cond (is-old-es-style
           (progn
             (mapc (lambda (f) (insert "import " f " from './" f "';\n")) fs)
             (insert "\n")
             (insert "export default {\n")
             (mapc (lambda (f) (insert "    " f ",\n")) fs)
             (insert "};")))
          (t
           (mapc (lambda (f) (insert "export { default as " f " } from './" f "';\n")) fs)))))
Convert expression into template string

Converts an expression into a template string.

Example: When you would call the function on the foo inside the console.log, It would wrap it like this console.log(`${foo}`).

(defun +js|convert-sexp-to-template-string ()
  "Wrap sexp into a template string"
  (interactive)
  (kill-sexp)
  (insert (concat "`${" (substring-no-properties (car kill-ring)) "}`"))
  (pop kill-ring))
Split / Join JSX Tag Node

Stolen from the vscode extension: dannyconnell/vscode-split-html-attributes I don’t want to translate this to elisp, and js works as well.

Split/Joins a JSX tags by seperating the attributes

const useSpacesForTabs = true;
const tabSize = 4;
const sortOrder = [];
const closingBracketOnNewLine = true;

let input = process.argv[2]

// count the number of lines initally selected
let lineCount = input.split('\n').length
let lineText = input

// get the initial white space at the start of the line
let initialWhitespaceRegex = /\s*/i
let initialWhitespace = lineText.match(initialWhitespaceRegex)[0]

// get the opening tag
let openingTagRegex = /^[^\s]+/
let openingTag = input.match(openingTagRegex)[0]

// remove opening tag and trim
input = input.replace(openingTagRegex, '')
input = input.trim()

// get the ending bracket (if it's a "/>")
let endingBracket = ''
if (input.endsWith('/>')) {
    endingBracket = '/>'
}
else {
    endingBracket = '>'
}

// remove ending bracket and trim
if (endingBracket == '/>') {
    input = input.replace('/>', '')
}
else {
    input = input.substring(0, input.length - 1)
}
input = input.trim()

// create the indentation string
let indentationString
if (useSpacesForTabs == false) {
    indentationString = '\t'
}
else {
    indentationString = ' '.repeat(tabSize)
}

// regex to select all spaces that aren't within quotes
let spacesRegex = /\s+(?=([^"]*"[^"]*")*[^"]*$)/g

// get attributes into an array
let attributesString = input.replace(spacesRegex, '\n')
let attributesArray = attributesString.split('\n')

// sort the attributes array
let sortedAttributesArray = []
if (sortOrder.length) {

    // loop through sortOrder array
    sortOrder.forEach(search => {
        // loop through attributesArray
        let itemsToMove = []
        attributesArray.forEach((item, index) => {
            if (item.match(search)) {
                itemsToMove.push(index)
                // attributesArray.splice(index, 1)
            }
        })
        // move matched items from attributesArray to sortedAttributesArray (and sort them)
        let tempMatchedItems = []
        itemsToMove.forEach(indexItem => {
            tempMatchedItems.push(attributesArray[indexItem])
        })
        tempMatchedItems.sort()
        sortedAttributesArray.push(...tempMatchedItems)

        // remove matched items from attributesArray
        for (var i = itemsToMove.length - 1; i >= 0; --i) {
            attributesArray.splice(itemsToMove[i], 1)
        }
    })

    // sort remaining attributes and add to sortedAttributesArray
    attributesArray.sort()
    sortedAttributesArray.push(...attributesArray)
}
else {
    sortedAttributesArray = attributesArray
}

// add the opening tag
let result = openingTag

// set the join character based on number of lines initially selected
// (newLine if one line, space if more)
let joinCharacter = lineCount > 1 ? ' ' : '\n'

// if there are no attributes, set joinCharacter to ''
if (sortedAttributesArray.length == 1 && sortedAttributesArray[0] == '') {
    joinCharacter = ''
}

// add the sorted attributes to the textSplit string
if (lineCount > 1) {
    sortedAttributesArray.forEach(item => {
        result += joinCharacter + item
    })
}
else {
    sortedAttributesArray.forEach(item => {
        result += joinCharacter + initialWhitespace + indentationString + item
    })
}

// configure ending bracket (new line or not new line)
if (lineCount > 1) {
    if (endingBracket == '/>') {
        endingBracket = ' ' + endingBracket
    }
}
else {
    if (closingBracketOnNewLine) {
        endingBracket = '\n' + initialWhitespace + endingBracket
    }
    else if (endingBracket == '/>') {
        endingBracket = ' ' + endingBracket
    }
}

// add the ending bracket
result = result + endingBracket

console.log(result)
(defun +rjsx/find-opening-closing-tag ()
  "Return the opening and closing carent character positions for a tag under the cursor."
  (save-excursion
    (let* ((is-evil-normal-state (evil-normal-state-p))
           (opening
            (or (-some->> (rjsx--tag-at-point)
                  (js2-node-abs-pos))
                ;; Fallback when js2-node-abs-pos parser fails
                (save-excursion (search-backward "<"))))
           (closing (progn
                      (when is-evil-normal-state
                        (evil-insert-state))
                      (while (and
                              (re-search-forward "[^=]>" (point-max) t nil)
                              (progn (backward-char 1) t)
                              (rjsx--tag-at-point)
                              (not (eq (js2-node-abs-pos (rjsx--tag-at-point)) opening))
                              (progn (forward-char 1) t)))
                      (when is-evil-normal-state
                        (evil-normal-state))
                      (forward-char 2)
                      (point))))
      (cons opening closing))))

(defun +rjsx/split-join ()
  "Function docstring"
  (interactive)
  (when (eq ?> (char-after))
    (backward-char 1))
  (-let* ((bounds (cond
                   ((use-region-p) (cons (region-beginning) (region-end)))
                   (t (+rjsx/find-opening-closing-tag))))
          ((beg . end) bounds)
          (input (->> (buffer-substring-no-properties beg end)
                   (s-replace "\n" "\\n")
                   (s-replace "'" "\\'")))
          (output (->> (shell-command-to-string (t! "node <<doom-private-dir>>/.cache/split-join-tag.js  $'<<input>>'"))
                    (s-trim-right))))
    (delete-region beg end)
    (insert output)
    (indent-region beg
                   ;; End position might be filled by whitespace, so we search again for the ending carent
                   (save-excursion (progn
                                     (goto-char (cdr (+rjsx/find-opening-closing-tag))))))))
Expand self closing tag

Converts self closing JSX tags to closing tags. <Foo /> -> <Foo>|</Foo>

(defun +rjsx|expand-insert-self-closing-tag ()
  "Opens the current tag at any position of the cursor and starts insert mode"
  (interactive)
  (search-forward "/>")
  (evil-backward-char)
  (call-interactively #'delete-backward-char)
  (call-interactively #'rjsx-electric-gt)
  (newline)
  (call-interactively #'evil-indent-line)
  (call-interactively #'evil-open-above))
Extract Props from function arguments to body
(defun +js|extract-props ()
  "Extract props object under the cursor."
  (interactive)
  (save-excursion
    (let* ((point-start (search-backward "{"))
           (point-end (search-forward "}"))
           (text (buffer-substring-no-properties point-start point-end)))
      (delete-region point-start point-end)
      (insert "props")
      (evil-open-below 1)
      (insert (template "const <<text>> = props;"))
      (search-backward "}")
      (js2r-expand-node-at-point)))
  (evil-normal-state))
Company Files

Remove the js extension for company-files.

(defun company-js-files (command &optional arg &rest ignored)
  "Company complete path. Remove extension after completion"
  (interactive (list 'interactive))
  (require 'company)
  (cl-case command
    (interactive (company-begin-backend 'company-js-files))
    (prefix (company-files--grab-existing-name))
    (candidates (company-files--complete arg))
    (location (cons (dired-noselect
                     (file-name-directory (directory-file-name arg))) 1))
    (post-completion (when (s-matches? "\.js$" arg) (delete-backward-char 3)))
    (sorted t)
    (no-cache t)))

(map! :map js2-mode-map
      :i "C-x C-f" #'company-js-files)
Import JS File
(defun +js/import-file (file)
  (let ((cursor-postion (point))
        (filename (f-no-ext file)))
    (insert (template "import  from '<<filename>>';"))
    (goto-char cursor-postion)
    (forward-char 7)
    (evil-insert-state)))

(defun +js|ivy-import-file (&optional action)
  (interactive)
  (let* ((local-files
          (-->
           (-concat (list find-program) counsel-file-jump-args)
           (string-join it " ")
           shell-command-to-string
           split-string))
         (node-packages
          (-->
           (concat "jq -r '.dependencies | keys | .[]' " (concat (projectile-project-root) "package.json"))
           shell-command-to-string
           split-string))
         (imports (append local-files node-packages)))
    (ivy-read "Import file " imports :action (or action '+js/import-file))))
Switch Ternary
(defun js2r-ternary-switch-statements ()
  "Switch expressions in a ternary."
  (interactive)
  (js2r--guard)
  (js2r--wait-for-parse
   (save-excursion
     (let* ((ternary (js2r--closest 'js2-cond-node-p))
            (test-expr (js2-node-string (js2-cond-node-test-expr ternary)))
            (true-expr (js2-node-string (js2-cond-node-true-expr ternary)))
            (false-expr (js2-node-string (js2-cond-node-false-expr ternary)))
            (stmt (js2-node-parent-stmt ternary))
            (stmt-pre (buffer-substring (js2-node-abs-pos stmt) (js2-node-abs-pos ternary)))
            (stmt-post (s-trim (buffer-substring (js2-node-abs-end ternary) (js2-node-abs-end stmt))))
            (beg (js2-node-abs-pos stmt)))
       (goto-char beg)
       (delete-char (js2-node-len stmt))
       (insert "return " test-expr)
       (newline)
       (insert "? " false-expr)
       (newline)
       (insert ": " true-expr ";")
       (indent-region beg (point))))))
Ignore Flycheck error on line
(defun +js|eslint-fix-ignore-error ()
  "Adds an ignore with the current flycheck error."
  (interactive)
  (if-let ((error-id (flycheck-copy-errors-as-kill (point) #'flycheck-error-id)))
    (save-excursion
      (previous-line)
      (end-of-line)
      (newline-and-indent)
      (insert (template "// eslint-disable-next-line <<error-id>>")))))
Find file from package.json root
(defun +javscript|package-root-files ()
  "Find any file relative to the upmost package.json,
useful for repos that contain multiple packages."
  (interactive)
  (let ((default-directory
          (f--traverse-upwards (f-exists? (f-expand "package.json" it)))))
    (+default/find-file-under-here)))
Find node_modules package

Go to a package from the node_modules directory.

(defun +javascript/find-npm-package-goto (package)
  "Go to directory by package name"
  (-some->> (f-join (projectile-project-root) "node_modules")
    (-id-when #'f-exists?)
    (-f-join package)
    (find-file)))

(defun +javascript|find-npm-package ()
  "Find package in node_modules directory."
  (interactive)
  (-when-let* ((json (-some->> (f-join (projectile-project-root) "package.json")
                       (-id-when #'f-exists?)
                       (json-read-file)))
               ((&alist 'dependencies dependencies
                        'devDependencies devDependencies) json)
               (packages (->> (-concat dependencies devDependencies)
                              (-map #'car))))
    (ivy-read "Go to package directory: " packages
              :action #'+javascript/find-npm-package-goto)))

Bindings

(map!
 :after js2-mode
 :map js2-mode-map
 :desc "Goto parent function" :n "gh" (cmd! (js2-beginning-of-defun))
 :localleader
 :desc "Goto NPM Package" "m" #'+javascript|find-npm-package
 :desc "Package Root Files" "SPC" #'+javscript|package-root-files
 (:prefix-map ("c" . "Create")
  :desc "Import File" "i" #'js-import))

(map!
 :after rjsx-mode
 :map rjsx-mode-map
 :localleader
 (:desc "Open Self-Closing Tag" :n ">" #'+rjsx|expand-insert-self-closing-tag)
 (:desc "Rename Tag" :n "," #'rjsx-rename-tag-at-point)
 (:desc "Collapse/Expand Tag" :n "<" #'+rjsx/split-join))
Evil Function Text Object Motion

Adds text objects for functions in javascript. So you can press daf to delete a function.

(add-hook! js-mode
  (require 'evil-text-objects-javascript)
  (evil-text-objects-javascript/install))
Evil Method Text Object Motion

Overwrite evil-text-objects-javascript to also accepts methods.

I mainly changed the function marking helper.

- (call-interactively #'mark-defun)
+ (call-interactively #'js2-mark-defun)
(after! evil-text-objects-javascript
  (evil-define-text-object
    evil-inner-javascript-function (count &optional beg end type)
    "inner text object for all javascript functions."
    (call-interactively #'js2-mark-defun)
    (narrow-to-region (region-beginning) (region-end))
    (goto-char (point-min))
    (let* ((beg (save-excursion
                  (search-forward "(")
                  (backward-char)
                  (evil-jump-item)
                  (search-forward-regexp "[({]")
                  (point)))
           (end (save-excursion
                  (goto-char beg)
                  (evil-jump-item)
                  (point))))
      (evil-range beg end type)))

  (evil-define-text-object
    evil-outer-javascript-function (count &optional beg end type)
    "Outer text object for all Javascript functions."
    (call-interactively #'js2-mark-defun)
    (narrow-to-region (region-beginning) (region-end))
    (goto-char (point-min))
    (let* ((beg (save-excursion
                  (when (looking-at "[[:space:]]")
                    (evil-forward-word-begin))
                  (point)))
           (end (save-excursion
                  (goto-char beg)
                  (search-forward "(")
                  (backward-char)
                  (evil-jump-item)
                  (search-forward-regexp "[({]")
                  (evil-jump-item)
                  (forward-char)
                  (if (save-excursion
                        (progn
                          (forward-char)
                          (when (looking-at ",") (point))))
                    (point)))))
      (evil-range beg end type))))

Nix

Packages

Nixpkgs Fmt
Package
(package! nixpkgs-fmt)
Config
(use-package nixpkgs-fmt
  :after nix-mode
  :config
  (setq nixpkgs-fmt-on-save-mode t)
  :init
  (add-hook! nix-mode #'nixpkgs-fmt-on-save-mode))

Functions

Edit indirect

Edit nix script regions in an indirect buffer just like org-edit-special.

(defun my-nixos/edit-indirect ()
  "Edit script in an indirect buffer."
  (interactive)
  (and-let* ((beg (save-excursion
                    (search-backward "''\n" nil t)
                    (forward-char 3)
                    (point)))
             (end (save-excursion
                    (re-search-forward "''" nil t)
                    (previous-line 1)
                    (goto-char (point-at-eol))
                    (point))))
    (+indirect-indent|edit beg end #'sh-mode)))

(map! :map nix-mode-map "C-c '" 'my-nixos/edit-indirect)

(set-popup-rule! "^\\*edit-indirect" :ignore t)

Haskell

Functions
Autofix with brittany
(defun my-haskell|autofix ()
  "Function formats haskell buffer with brittany on save."
  (interactive)
  (when (eq major-mode 'haskell-mode)
    (shell-command-to-string (format "brittany --config-file .brittany.yaml --write-mode inplace %s" buffer-file-name))
    (revert-buffer :ignore-auto :noconfirm)
    (haskell-mode-stylish-buffer)
    (save-buffer)))

Markdown

Config

(use-package! markdown-mode
  :init
  (add-to-list 'auto-mode-alist '("\\.mdx\\'" . markdown-mode))
  (setq markdown-fontify-code-blocks-natively t)
  :config
  (add-hook! markdown-mode
    (hl-line-mode -1)
    (visual-line-mode)
    (visual-fill-column-mode)
    (outline-minor-mode)
    (setq markdown-hide-urls t)
    (setq visual-fill-column-width 90
          display-line-numbers nil)
    (setq line-spacing 2
          fill-column 80)))

Bindings

(map! (:map markdown-mode-map
       :n "<"    #'markdown-promote
       :n ">"    #'markdown-demote))

Astro

Astro Mode

(define-derived-mode astro-mode web-mode "astro")
(setq auto-mode-alist
      (append '((".*\\.astro\\'" . astro-mode))
              auto-mode-alist))

(with-eval-after-load 'lsp-mode
  (add-to-list 'lsp-language-id-configuration
               '(astro-mode . "astro"))

  (lsp-register-client
   (make-lsp-client :new-connection (lsp-stdio-connection '("astro-ls" "--stdio"))
                    :activation-fn (lsp-activate-on "astro")
                    :server-id 'astro-ls)))

Lisp

Sort sexp

(defun my-lisp|sort-sexp ()
  "Sort sexps between point and end of current sexp.
  With multilines sexp, comments and blank lines are considered part of the following
  sexp. Comments at the end of a line (after some other content)
  are considered part of the preceding sexp."
  (interactive)
  (save-excursion
    (save-restriction
      (up-list)
      (backward-sexp)
      (let* ((map? (looking-at "{"))
             (start (1+ (point)))
             (end (progn (forward-sexp)
                         (1- (point))))
             (multiline? (< (line-number-at-pos start)
                            (line-number-at-pos end)))
             ;; Should really check every line, but the only the first with a sexp should be enough
             (one-sexp-per-line? (save-excursion
                                   (goto-char start)
                                   ;; Skip start comment, if any
                                   (let ((sexp2-end (progn (clojure-forward-logical-sexp 2) (point)))
                                         (sexp2-start (progn (clojure-forward-logical-sexp -2) (point))))
                                      (< (line-number-at-pos sexp2-start)
                                       (line-number-at-pos sexp2-end))))))
        (narrow-to-region start end)
        ;; Delete last empty line, if any
        (save-excursion
         (beginning-of-line)
         (when (looking-at "[[:blank:]]*$")
          (backward-char)
          (delete-region (point) (point-max))))

        (goto-char start)
        (sort-subr nil
               (lambda ()
                     (if (and multiline? one-sexp-per-line?)
                      (progn (message "forward line")
                           (forward-line))
                      (search-forward-regexp "[^:blank:]" nil :no-error)))
               ;; Move to end of current top-level thing.
               (lambda ()
                     (condition-case nil
                       (while t (up-list))
                       (scan-error nil))
                   ;; We could be inside a symbol instead of a sexp.
                     (unless (looking-at "\\s-\\|$")
                      (clojure-forward-logical-sexp))
                     (when (and multiline? one-sexp-per-line?)
                     ;; move past comments at the end of the line.
                      (search-forward-regexp "$")))

                 ;; Start of record
               (lambda ()
                     (when multiline?
                      (skip-chars-forward "\r\n[:blank:]")
                      (comment-forward)
                      (skip-chars-forward "[:blank:]\n\r[("))
                     (condition-case nil
                       (progn (clojure-forward-logical-sexp)
                              (forward-sexp -1))
                       (scan-error nil))

                     nil)

                 ;; End of record
               (lambda ()
                     (clojure-forward-logical-sexp (if map? 2 1)))

               (lambda (a b)
                     (let* ((s1 (buffer-substring (car a) (cdr a)))
                            (s2 (buffer-substring (car b) (cdr b)))

                            ;; Ignore namespaces for keywords
                            (s1* (replace-regexp-in-string
                                  "^::" ""
                                  (if (string-match "^::.*/\\(.*\\)" s1)
                                      (match-string 1 s1)
                                   s1)))
                            (s2* (replace-regexp-in-string
                                  "^::" ""
                                  (if (string-match "^::.*/\\(.*\\)" s2)
                                      (match-string 1 s2)
                                   s2))))
                       (string< s1* s2*))))
        ;; Delete starting whitespaces
        (goto-char (point-min))
        (delete-region (point) (progn (skip-chars-forward "[:blank:]") (point)))
        ;; Go to the end and insert newline if we end with a comment.
        (goto-char (point-max))
        (when (nth 4 (parse-partial-sexp (point-min) (point)))
         (insert "\n"))
        ;; Clean superfluous spaces
        (when (not multiline?)
         (goto-char (point-min))
         (while (re-search-forward "\\s-+" nil t)
          (replace-match " ")))
        ;; Fill paragraph
        (when (and (not map?) (not one-sexp-per-line?))
         (fill-region-as-paragraph start end))))
    ;; Indent
    (let ((end (progn (up-list) (point)))
          (start (progn (progn (clojure-forward-logical-sexp -1)) (point))))
      (indent-region start end))))

Clojure / Clojurescript

Packages

clj-kondo
(use-package! flycheck-clj-kondo
  :when t
  :after flycheck)
hiccup-cli
Package
(package! hiccup-cli)
Config
(use-package! hiccup-cli)
Cider
Package
(package! cider :pin "810337cee931d9f14aa16550a74516f8337a47f3")
Config
(defun my-cider/init-faces ()
  (set-face-attribute 'cider-error-overlay-face nil
                      :foreground (face-attribute 'custom-state :foreground)
                      :background (face-attribute 'custom-state :background))
  (set-face-attribute 'cider-error-highlight-face nil
                      :underline `(:color ,(face-attribute 'shadow :foreground)
                                   :style wave)))

(defun my-cider/clear-errors (&rest _)
  (when cider-mode
    (cider-clear-compilation-highlights t)))

(use-package! cider
  :config
  (setq cider-repl-pop-to-buffer-on-connect nil)
  (setq cider-reuse-dead-repls t)
  ;; Show errors inline
  (setq cider-use-overlays t)
  (setq cider-use-fringe-indicators nil)
  (setq cider-show-error-buffer nil)
  (setq cider-auto-select-error-buffer nil)
  (setq cider-words-of-inspiration nil)
  (add-hook 'doom-load-theme-hook #'my-cider/init-faces)
  (advice-add 'doom/escape :before #'my-cider/clear-errors))
Hacks
Fix cider for 28.1

Problem starting cider in clj/cljs `(cl-no-applicable-method map-into ((right-margin 80)) hash-table)` · Issue #3029 · clojure-emacs/cider

Fixes cider from breaking with newest emacs, for this isseu

condition-case: Error in a Doom startup hook: nrepl-connected-hook, cider–connected-handler, (cl-no-applicable-method map-into ((“nrepl.middleware.print/stream?” “1”)) (hash-table :test equal))

(package! map :pin "bb50dba")
Functions
Fix cider-connect checking .authinfo.gpp -> password
(defun my-cider/connect (connect-fn &optional params)
  (require 'noflet)
  (let* ((buffer (noflet ((cider--ssh-hosts () nil))
                   (apply connect-fn params))))
    (persp-add-buffer buffer)))

(defun my-cider|connect-clj (&optional params)
  "Custom cider connect function that doesn't trigger authinfo.gpg password query."
  (interactive "P")
  (my-cider/connect #'cider-connect-clj params))

(defun my-cider|connect-cljs (&optional params)
  "custom cider-connect-cljs function that doesn't trigger authinfo.gpg password query."
  (interactive "P")
  (my-cider/connect #'cider-connect-cljs params))
Print comment fix
  • Make cider eval work with evil-mode
  • Fix result insertion to insert after the sexp not inside
  • Pprint maps
(defun my-cider/eval-jump-opening-brace ()
  (when (eq (string-to-char "(") ;; Workaround for parinfer inferring ?( as opening brace
            (char-after))
    (evil-jump-item)))

(defun my-cider/eval-print-handler (&optional buffer)
  "Make a handler for evaluating and printing result in BUFFER."
  ;; NOTE: cider-eval-register behavior is not implemented here for performance reasons.
  ;; See https://github.com/clojure-emacs/cider/pull/3162
  (nrepl-make-response-handler (or buffer (current-buffer))
                               (lambda (buffer value)
                                 (with-current-buffer buffer
                                   (forward-char 1)
                                   (insert
                                    " "
                                    (if (derived-mode-p 'cider-clojure-interaction-mode)
                                        (format "\n%s\n" value)
                                      value))
                                   (backward-char 1)
                                   (ignore-errors
                                     (when (string= (string (char-after (point))) "}")
                                       (evil-jump-item)
                                       (cider-format-edn-last-sexp)))))
                               (lambda (_buffer out)
                                 (cider-emit-interactive-eval-output out))
                               (lambda (_buffer err)
                                 (cider-emit-interactive-eval-err-output err))
                               ()))

(defun my-cider|cider-eval-last-sexp (&optional output-to-current-buffer)
  (interactive "P")
  (let ((previous-evil-state evil-state))
    (save-excursion
      (my-cider/eval-jump-opening-brace)
      (when (eq previous-evil-state 'normal)
        (evil-insert-state)
        (forward-char 1))
      (cider-interactive-eval nil
                              (when output-to-current-buffer (my-cider/eval-print-handler))
                              (cider-last-sexp 'bounds)
                              (cider--nrepl-pr-request-map))
      ;; (cider-eval-last-sexp output-to-current-buffer)
      ;; Dont restore state when doing eval when inserting result
      ;; The `cider-eval-print-handler' function is async,
      (when (eq previous-evil-state 'normal)
        (evil-normal-state)))))

(defun my-cider|pprint-eval-last-sexp-to-comment (&optional insert-before)
  "Pointer position fix for evil normal state."
  (interactive "P")
  (my-cider/eval-jump-opening-brace)
  (when (eq evil-state 'normal)
    (forward-char 1))
  (call-interactively #'cider-pprint-eval-last-sexp-to-comment))
Cleanup dead cider buffers
(defun my-cider|cleanup-cider-buffers ()
  (let* ((buffers (->> (buffer-list)
                       (-filter (lambda (b) (->> (buffer-name b)
                                                 (substring-no-properties)
                                                 (s-starts-with? "*cider-repl")))))))
    (-each buffers #'kill-buffer)))
Bindings
(defun my-cider|eval-pp (&rest args)
  (interactive)
  (let* ((buf (current-buffer)))
    (evil-append 0)
    (cider-pprint-eval-last-sexp)
    (with-current-buffer buf
      (backward-char 2)
      (evil-normal-state))))

(use-package! clojure-mode
  :config
  (map! :map (clojurescript-mode-map clojure-mode-map clojurec-mode-map)
        :desc "Indent" :n "C-<tab>" (cmd!
                                     (save-excursion
                                       (save-evil-excursion
                                        (beginning-of-line)
                                        (insert "  "))))
        :desc "Indent" :n "C-<iso-lefttab>" (cmd!
                                             (save-excursion
                                               (save-evil-excursion
                                                (beginning-of-line)
                                                (when (looking-at "^  ")
                                                  (delete-char 2)))))
        :desc "Eval" :n "RET" #'my-cider|cider-eval-last-sexp
        :desc "Eval" :n "C-c C-p" #'my-cider|eval-pp
        :desc "Eval" :n "DEL" #'my-eval-function-and-last-marker)

  (map! (:localleader
         (:map (clojure-mode-map clojurescript-mode-map clojurec-mode-map)
               "c"  #'my-cider|connect-clj
               "C"  #'my-cider|connect-cljs
               (:prefix ("m" . "modify")
                :desc "Toggle Comment / Ignore" "t" #'clojure-toggle-ignore
                :desc "Toggle Comment / Ignore" "T" #'clojure-toggle-ignore-surrounding-form
                :desc "Thread first" "f" #'lsp-clojure-thread-first)
               (:prefix ("e" . "eval")
                :desc "Eval last sexp to comment" "c" #'my-cider|pprint-eval-last-sexp-to-comment)
               (:prefix ("i" . "inspect/insert")
                        "e" #'cider-enlighten-mode
                        "i" #'cider-inspect
                        "n" #'clojure-insert-ns-form
                        "r" #'cider-inspect-last-result)
               :localleader
               "x" #'cider-scratch)
         (:map (clojurescript-mode-map)
               (:prefix ("i" . "inspect")
                        "i" #'my-cider|inspect-expr)))))
Package
(package! lispyville :recipe (:host github :repo "noctuid/lispyville"))
Config
(use-package! lispyville
  :hook
  ((emacs-lisp-mode . lispyville-mode)
   (lisp-mode . lispyville-mode)
   (clojure-mode . lispyville-mode)
   (clojurescript-mode . lispyville-mode)
   (cider-repl-mode . lispyville-mode))
  :init
  (setq lispyville-key-theme
        '(
          ;; operators
          ;; text-objects
          wrap
          raise
          c-w
          ;; (prettify insert)

          commentary

          slurp/barf-cp
          additional
          additional-insert
          additional-motions
          (additional-wrap normal visual insert)))
  :config
  (lispyville-set-key-theme)
  ;; Override paragraph jumping from additional key theme
  (map! :map lispyville-mode-map
        :nm "{" #'evil-backward-paragraph
        :nm "}" #'evil-forward-paragraph
        :nm "C-<" (cmd!
                   (save-excursion
                     (evil-first-non-blank)
                     (backward-char 1)
                     (lispyville-< 1)))
        :nm "C->" (cmd!
                   (save-excursion
                     (evil-first-non-blank)
                     (backward-char 1)
                     (lispyville-> 1)))
        :nm "M-h" #'lispyville-backward-sexp-begin
        :nm "M-l" #'lispyville-forward-sexp-begin
        :nm "M-H" #'lispyville-beginning-of-defun
        :nm "M-L" #'lispyville-end-of-defun
        :nm "M-J" #'my-lispyville|drag-2-forward
        :nm "M-K" #'my-lispyville|drag-2-backward))
Jumps

Define jump points for lispyville functions

(advice-add #'lispyville-beginning-of-defun :around #'doom-set-jump-a)
(advice-add #'lispyville-end-of-defun :around #'doom-set-jump-a)
(advice-add #'lispyville-backward-up-list :around #'doom-set-jump-a)
(advice-add #'lispyville-up-list :around #'doom-set-jump-a)
(advice-add #'lispyville-insert-at-beginning-of-list :around #'doom-set-jump-a)
(advice-add #'lispyville-insert-at-end-of-list :around #'doom-set-jump-a)
(advice-add #'lispyville-open-below-list :around #'doom-set-jump-a)
(advice-add #'lispyville-open-above-list :around #'doom-set-jump-a)
Functions
Drag two items forward/backward
(defun my-lispyville/forward-let (n)
  (dotimes (_ n)
    (if (looking-at "(")
        (progn
          (evil-jump-item)
          (lispyville-forward-sexp-alt2 1))
      (lispyville-forward-sexp-alt2 1))))

(defun my-lispyville|drag-2-backward ()
  "Drags two items backward, useful for maps to drag the key and the value around."
  (interactive)
  (evil-first-non-blank)
  (my-parinfer/save-excursion
   (lispyville--drag #'lispy-move-up 2)
   (my-lispyville/forward-let 3)
   (lispyville--drag #'lispy-move-up 2)
   (lispyville-backward-sexp)))

(defun my-lispyville|drag-2-forward ()
  "Drags two items backward, useful for maps to drag the key and the value around."
  (interactive)
  (my-parinfer/save-excursion
   (lispyville--drag #'lispy-move-down 2)
   (lispyville-backward-sexp)
   (lispyville--drag #'lispy-move-down 2)
   (lispyville-backward-sexp)))
Package
(package! jet :recipe (:host github :repo "ericdallo/jet.el"))
Config
(use-package! jet)
Disable clj-refactor
(package! clj-refactor :disable t)

API

Let statements
(defun my-clojure/point-at-let-beg? ()
  (let ((p nil))
    (save-excursion
      (lispyville-backward-up-list 2)
      (setq p (re-search-forward "(?let \\[" (+ (point) 6) t)))
    p))

(defun my-clojure/inside-let-vector? ()
  (and (clojure--inside-let-binding-p)
       (my-clojure/point-at-let-beg?)))

Functions

Keywords to map
(defun my-clojure|region-keywords-to-map ()
  "Convert keywords in region to a clojure map."
  (interactive)
  (let* ((beg (region-beginning))
         (end (region-end))
         (input (buffer-substring-no-properties beg end))
         (output (->> input
                      (s-replace-all '(("(" . "")
                                       (")" . "")
                                       ("[" . "")
                                       ("]" . "")))
                      (s-split "\s")
                      (-sort #'string<)
                      (-map (fn! (concat ":" % " " %)))
                      (s-join (if (> (length input) 20) "\n" " "))
                      ((lambda (x) (s-wrap x "{" "}"))))))
    (save-excursion
      (my-parinfer/save-excursion
       "paren"
       (progn
         (delete-region beg end)
         (insert output))))))
Key formatting sorting
(defun my-clojure::format|sort-keys ()
  "Convert set of keys to multiline."
  (interactive)
  (-let* (((beg end) (evil-select-inner-object 'lispyville-list nil nil nil))
          (text (buffer-substring-no-properties (+ beg 1) (- end 1)))
          (new-text (->> text
                         (s-split " ")
                         (-sort #'string<)
                         (s-join " "))))
    (save-excursion
      (delete-region beg end)
      (goto-char beg)
      (insert (concat "[" new-text "]")))))

(defun my-clojure::format|keys-to-multi-line ()
  "Convert set of keys to multiline."
  (interactive)
  (-let* (((beg end) (evil-select-inner-object 'lispyville-list nil nil nil))
          (text (buffer-substring-no-properties (+ beg 1) (- end 1)))
          (new-text (->> text
                         (s-split " ")
                         (-sort #'string<)
                         (s-join "\n"))))
    (save-excursion
      (my-parinfer/save-excursion
       "paren"
       (progn
         (delete-region beg end)
         (goto-char beg)
         (insert (concat "[" new-text "]"))
         (indent-region (- 1 beg) (+ 1 end)))))))
Spy
(defun my-clojure/spy-buffer-fns ()
  "Get buffer functions"
  (-map
   (lambda (x) (->> x
                    (cdr)
                    (car)
                    (substring-no-properties)
                    ((lambda (x) (s-split-up-to ": " x 1)))
                    (cdr)
                    (car)))
   (counsel--imenu-candidates)))

(defun my-clojure|spy ()
  "Wrap element with spy function and insert function into file if not already available in namespace."
  (interactive)
  (save-excursion
    (let* ((fns (my-clojure/fns))
           (spy? (-contains? fns "spy")))
     (lispyville-wrap-round t)
     (insert "spy ")
     (when (not spy?)
       (lispyville-backward-function-begin)
       (insert "(defn spy" "\n"
               "  ([val] (js/console.log val) val)" "\n"
               "  ([val desc] (js/console.log desc val) val))" "\n" "\n")))))
Inspect cleaned up expression
(defun my-cider|inspect-expr (expr ns)
  "Evaluate EXPR in NS and inspect its value.
Interactively, EXPR is read from the minibuffer, and NS the
current buffer's namespace."
  (interactive (list (cider-read-from-minibuffer "Inspect expression: " (cider-sexp-at-point))
                     (cider-current-ns)))
  (setq cider-inspector--current-repl (cider-current-repl))
  (when-let* ((value (cider-sync-request:inspect-expr
                      expr ns
                      cider-inspector-page-size
                      cider-inspector-max-atom-length
                      cider-inspector-max-coll-size)))
    (cider-inspector--render-value value)
    (with-current-buffer cider-inspector-buffer
      (let ((content (->> (buffer-substring-no-properties (point-min) (point-max))
                          (s-replace "\nInspector error for: " "")
                          (s-replace-regexp ",$" ""))))
        (read-only-mode -1)
        (erase-buffer)
        (insert content)
        (zprint-format-buffer)
        (clojurescript-mode)
        (goto-char (point-min))))))
Thread logging

Inserts doto println depending on thread first/last. Used in thread_spy snippet

(defun my-clojure/thread-spy ()
  (let ((log-fn (if (derived-mode-p 'clojurescript-mode) "js/console.log" "println"))
        (p (point)))
    (save-excursion
      (search-backward "->" nil t)
      (let* ((thread-first? (string= (symbol-at-point) "->")))
        (if thread-first?
            (format "(doto %s)" log-fn)
          (format "(#(doto %% %s))" log-fn))))))
Namespace functions
(defun my-clojure/current-namespace ()
  (let ((ns (funcall clojure-expected-ns-function)))
    (if (string-empty-p ns)
        (->> (f-filename (buffer-file-name))
             (f-no-ext)
             (s-dashed-words))
      ns)))

(defun my-clojure/target-test-namespace ()
  (->> (my-clojure/current-namespace)
       (s-replace-regexp "-test\\|spec$" "")))

(defun my-clojure|update-ns-dwim ()
  "Updates the ns no-matter where your cursor is, when non is existing insert a new one."
  (interactive)
  (save-excursion
    (goto-char (point-min))
    (if (re-search-forward "(ns " nil t)
        (progn
          (sp-kill-sexp)
          (insert (my-clojure/current-namespace)))
      (clojure-insert-ns-form))))
Get prev atom

Automatically get the previous atom name in the current file as the base Used for reset! snippet

(defun my-clojure/get-prev-atom-variable-name ()
  (save-excursion
   (-some-> (when (re-search-backward "(defonce \\(.+?\\) (atom" nil t)
              (match-string 1))
     (substring-no-properties))))
Newline
(defun my-clojure|newline-and-indent ()
  "Workaround for clojure when inserting a newline after a comment would indent the expression below weirdly."
  (interactive)
  (if (clojure--in-comment-p)
      (progn (newline nil t)
             (insert ";; "))
    (newline-and-indent)))
Testing shadow-cljs
(defun my-cljs|tests-run-all-tests ()
  (interactive)
  (call-interactively #'cider-eval-buffer)
  (cider-interactive-eval
   (concat "(cljs.test/run-all-tests #\"app.*-test\")"))
  (let* ((txt nil))
    (save-window-excursion
      (let ((buffer (cider-current-repl-buffer)))
        (with-current-buffer buffer
          (goto-char (point-max))
          (let* ((beg (re-search-backward "^Testing app\\." nil t))
                 (end (re-search-forward "^cljs.user>" nil t)))
            (setq txt (buffer-substring beg end))))))
    (with-output-to-temp-buffer "*Extracted Text*"
      (princ txt))
    (with-current-buffer "*Extracted Text*"
      (save-excursion
        (read-only-mode -1)
        (let ((beg (re-search-forward "^expected: "))
              (end (goto-char (point-at-eol))))
          (cider-format-edn-region beg end)))
      (clojure-mode)
      (flycheck-mode -1))))
Convert svg to uix
(defun my-clojure|svg-from-clipboard-as-uix ()
  (interactive)
  (let ((default-directory "/home/floscr/.config/dotfiles/new/modules/scripts"))
    (insert (shell-command-to-string "bb ./src/convert_html.clj"))))
Find reference
(defun my-clojure|find-reference ()
  "Automatically find references for the function when on the line of the function header.
I never want to find all the references of defn."
  (interactive)
  (save-excursion
    (if-let ((point-at-end-of-defn (save-excursion (progn
                                                     (beginning-of-line)
                                                     (search-forward-regexp "(defn-?" (point-at-eol) t)))))
        (progn
          (goto-char point-at-end-of-defn)
          (when (search-forward-regexp "\\^" (point-at-eol) t)
            ;; Go to possible map opening
            (forward-char 1))
          (lispyville-forward-sexp-begin)
          (lsp-find-references))
      (call-interactively #'lsp-find-references))))
Wait for connection
(cl-defun my-cider/wait-for-connection (&key (timeout 10) (interval 0.1))
  "Block until cider is connected in the current buffer"
  (while (and (> timeout 0)
              (not (cider-connected-p)))
    (sleep-for interval)
    (setq timeout (- timeout interval))))
Require other window
(defun my-clojure|require-other-window-ns ()
  "Add require statement with the ns from the other window."
  (interactive)
  (let ((other-window-ns
         (save-window-excursion
           (other-window 1)
           (my-clojure/current-namespace))))
    (progn
      (goto-char (point-min))
      (search-forward "(:require" nil t)
      (evil-open-below 1)
      (insert (format "[%s]" other-window-ns))
      (backward-char 1))))
Find require
(defun +clojure/find-require (ns)
  (save-excursion
    (goto-char (point-min))
    (re-search-forward ns nil t)))

(defun +clojure/find-component-system-require ()
  (cond
   ((+clojure/find-require "rumext.v2") :rum)
   (t :uix)))

(defun +clojure/find-test-require ()
  (when-let ((pos (or (+clojure/find-require "clj.test")
                      (+clojure/find-require "cljs.test")
                      (+clojure/find-require "clojure.test"))))
    (save-excursion
      (goto-char pos)
      (->> (substring-no-properties (thing-at-point 'line))
           (s-trim)
           (s-replace-regexp ")+$" "")))))

(defun +clojure/find-test-require-alias ()
  (require 'parseedn)
  (require 'treepy)
  (-some-> (+clojure/find-test-require)
           (parseedn-read-str)
           (treepy-vector-zip)
           (+treepy/walk-while (fn! (not (eq ':as %))))
           (treepy-next)
           (treepy-next)
           (treepy-node)))

(defun +clojure/with-test-require-alias ()
  (when-let (alias (+clojure/find-test-require-alias))
    (when (symbolp alias)
      (concat (symbol-name alias) "/"))))

(comment
 (with-current-buffer (find-buffer-visiting "/home/floscr/Code/Work/Hyma/tokens-studio-for-penpot/frontend/test/token_tests/token_test.cljs")
   (+clojure/find-test-require-alias))

 (with-current-buffer (find-buffer-visiting "/home/floscr/Code/Work/Hyma/tokens-studio-for-penpot/frontend/test/token_tests/logic/token_actions_test.cljs")
   (+clojure/with-test-require-alias))
 nil)
let statement
(defun my-clojure/let-ignore-prefix ()
  (ignore-errors
    (when (my-clojure/inside-let-vector?)
      "_ ")))

(defun my-clojure/prev-let-statement-line ()
  (save-excursion
    (previous-line)
    (-some->> (thing-at-point 'line t)
              (s-trim)
              (s-split " ")
              (first))))

Config

(use-package! clojure-mode
  :config
  (define-clojure-indent (alet 'defun) (mlet 'defun))
  (define-clojure-indent (css 'defun))
  (define-clojure-indent (describe 1) (it 1) (is 1)))
(use-package! lsp-mode
  :commands lsp
  :hook ((clojurescript-mode . lsp)
         (clojure-mode . lsp))
  :config
  (remove-hook 'clojure-mode-hook #'rainbow-delimiters-mode)
  (setq lsp-enable-file-watchers nil)
  (setq lsp-file-watch-ignored-directories
        (cl-union lsp-file-watch-ignored-directories
                  '("[/\\\\]build\\'"
                    "[/\\\\]deployment\\'"
                    "[/\\\\]public\\'"
                    "[/\\\\]resource\\'"
                    "[/\\\\]target\\'"
                    "[/\\\\]tmp\\'"
                    "[/\\\\].log\\'"
                    "[/\\\\].clj-kondo\\'"
                    "[/\\\\].git\\'"
                    "[/\\\\].shadow-cljs\\'"
                    "[/\\\\]src/js\\'"
                    "[/\\\\]src/less\\'"
                    "[/\\\\]src/tailwind\\'"
                    "[/\\\\]test/e2e/screenshots\\'"
                    "[/\\\\]ssh:\\'"))))

Themes

(after! clojure-mode
  (font-lock-add-keywords
   'clojurescript-mode
   `((,(concat "(\\(?:clojure.core/\\)?\\("
                (regexp-opt '("defui" "$" "defn*"))
                ;; Function declarations.
                "\\)\\>"
                ;; Any whitespace
                "[ \r\n\t]*"
                ;; Possibly type or metadata
                "\\(?:#^\\(?:{[^}]*}\\|\\sw+\\)[ \r\n\t]*\\)?"

                "\\(\\sw+\\)?")
      (1 font-lock-keyword-face)
      (2 font-lock-function-name-face nil t)))
   t))

Bindings

(map!
 :after lsp-mode
 :map lsp-mode-map)

(map!
 :map (clojure-mode-map clojurescript-mode-map)
 :desc "Newline" :i "RET" #'my-clojure|newline-and-indent
 :desc "Goto parent function" :n "gh" (cmd! (lispyville-backward-function-begin))
 :desc "Goto parent function" :n "[[" (cmd! (lispyville-backward-function-begin))
 :desc "Goto parent function" :n "]]" (cmd! (lispyville-forward-function-begin))
 :g [M-return] #'my-clojure|find-reference
 :localleader
 "s" #'clojure-sort-ns
 "S" #'my-lisp|sort-sexp
 "l" #'my-pitch|l10n-jump
 (:prefix ("m" . "modify")
          "f" #'lsp-clojure-thread-first
          "F" #'lsp-clojure-thread-first-all
          "l" #'lsp-clojure-thread-last
          "L" #'lsp-clojure-thread-last-all
          "u" #'lsp-clojure-unwind-thread))

(map!
 :map (clojurescript-mode-map)
 :localleader
 (:prefix ("t" . "test")
          "n" #'cljs-tests-run-all-tests))
Test rotation words
(add-hook! 'clojurescript-mode-hook
  (defun my-clojure/setup-test-file-vars ()
    "Sets up variables for test files (cypress, jest)."
    (when (-some->> (buffer-file-name)
                    (string-match "\\.*_spec.cljs$"))
      ;; Rotate it/it.only
      (add-to-list 'rotate-text-local-symbols '("it" "j/only")))))

Rust

(defun my-rust/edit-indirect ()
  "Edit script in an indirect buffer."
  (interactive)
  (and-let* ((beg (save-excursion
                    (search-backward "r#\"{" nil t)
                    (search-forward "{" nil t)
                    (backward-char 1)
                    (point)))
             (end (save-excursion
                    (re-search-forward "}\"#" nil t)
                    (search-backward "}" nil t)
                    (forward-char 1)
                    (point))))
    (+indirect-indent|edit beg end #'json-mode)))

(map! :map rust-mode-map "C-c '" 'my-rust/edit-indirect)

Tree Sitter

Config

(use-package! treesit
  :mode (("\\.tsx\\'" . tsx-ts-mode))
  :config
  (comment (pushnew! tree-sitter-major-mode-language-alist '(tsx-ts-mode . tsx))))

(add-hook! tsx-ts-mode 'lsp!)
(add-hook! typescript-ts-mode 'lsp!)

Repl

My playground for testing out tree sitter

(-> (tree-sitter-node-at-point)
    (tsc-get-parent)
    (tsc-node-start-position)
    (goto-char))

(tsc-node-eq
 (tree-sitter-node-at-point)
 (-> (tree-sitter-node-at-point)
     (tsc-get-parent)))

(cons
 (tsc-node-start-position (tree-sitter-node-at-point))
 (-> (tree-sitter-node-at-point)
     (tsc-get-parent)
     (tsc-node-start-position)))

(-> (tree-sitter-node-at-point)
    (tsc-get-next-sibling-)
    (tsc-node-start-position)
    (goto-char))

(defun my-treesitter|raise ()
  "Go to the parent context for the node at point."
  (interactive)
  (let* ((node (tree-sitter-node-at-point))
         (parent-node (tsc-get-parent node)))
    (if (and (not (= (point-min) (point)))
             (my-treesitter/node-point-eq node parent-node))
        (progn
          (backward-char 1)
          (my-treesitter|raise))
      (let* ((node-str (substring-no-properties))))
      (-> (tsc-node-start-position parent-node)
          (goto-char)))))

(-> (tree-sitter-node-at-point)
    (tsc-node-end-position)
    (goto-char))

Functions

Node point equality
(defun my-treesitter/node-point-eq (a b)
  (= (ts-node-start-position a)
     (ts-node-start-position b)))
Go to parent
(defun my-treesitter|goto-parent ()
  "Go to the parent context for the node at point."
  (interactive)
  (let* ((node (tree-sitter-node-at-point))
         (parent-node (tsc-get-parent node)))
    (if (and (not (= (point-min) (point)))
             (my-treesitter/node-point-eq node parent-node))
        (progn
          (backward-char 1)
          (my-treesitter/goto-parent))
      (-> (tsc-node-start-position parent-node)
          (goto-char)))))

Bindings

(defvar my-tree-sitter-mode-map (make-sparse-keymap))

(define-minor-mode my-tree-sitter-mode
  "A minor mode to enhance tree-sitter with additional keybindings."
  :lighter " TSEnhanced"
  :keymap my-tree-sitter-mode-map)

(add-hook 'tree-sitter-mode-hook (fn! (my-tree-sitter-mode 1)))

(map!
 :map my-tree-sitter-mode-map
 :desc "Goto parent function" :n "gh" #'my-treesitter|goto-parent)

Gptel

karthink/gptel: A simple LLM client for Emacs

To set up save your API token inside your authinfo.gpg file like this:

machine api.openai.com login apikey password TOKEN

Package

(package! gptel :pin "975c3e6")

Config

(use-package! gptel
  :commands gptel
  :config
  (gptel-make-anthropic "Claude"
    :stream t
    :key (fn! (auth-source-pick-first-password :host "api.anthropic.com")))
  (setq gptel-default-mode 'org-mode)
  (setq-default gptel-model "gpt-4")
  (setq gptel-directives
        `((default . "To assist:  Be terse.  Do not offer unprompted advice or clarifications. Speak in specific,
 topic relevant terminology. Do NOT hedge or qualify. Do not waffle. Speak
 directly and be willing to make creative guesses. Explain your reasoning. if you
 don’t know, say you don’t know.

 Remain neutral on all topics. Be willing to reference less reputable sources for
 ideas.

 Never apologize.  Ask questions when unsure.")
          (programmer . "You are a careful programmer.  Provide code and only code as output without any additional text, prompt or note.")
          (cliwhiz . "You are a command line helper.  Generate command line commands that do what is requested, without any additional description or explanation.  Generate ONLY the command, I will edit it myself before running.")
          (emacser . "You are an Emacs maven.  Reply only with the most appropriate built-in Emacs command for the task I specify.  Do NOT generate any additional description or explanation.")
          (explain . "Explain what this code does to a novice programmer.")
          ;; https://github.com/pitch-io/uix/blob/master/docs/chat-gpt.md
          (uix . "You are language to language translator. Your job is to translate code from JS, React, JSX to Clojure. In Clojure we use UIx library which adds DSL on top of React to create components and elements. The library provides uix.core namespace which includes top level api, as well as react hooks.
Components are created using defui macro, here’s the syntax: (defui component-name [props-map] body)
Elements are created using $ macro: ($ :dom-element optional-props-map …children)
Component names and props are written in kebab-case. Dom element keywords support hyper script syntax to define classes and id: :div#id.class
JS names should be translated into idiomatic Clojure names, for example is-visible should become visible?
Translate the following code to Clojure
You dont need to write the init functions.
You shoul impor uix from uix.core.")
          (playwright . "You are an expert level playwright integration test creator.
Answer quetions about the playwright API in a terse manner.
Double check if the provided API methods in your response are not halucinated and exist.")
          ,@(let ((res))
              (pcase-dolist (`(,sym ,filename)
                             '((Autoexpert "detailed-prompt.md")
                               (writer "writer-prompt.md")
                               (compress "spr-compress-prompt.md"))
                             res)
                (when-let* ((big-prompt (locate-user-emacs-file filename))
                            (_ (file-exists-p big-prompt)))
                  (push
                   `(,sym . ,(with-temp-buffer
                               (insert-file-contents big-prompt)
                               (goto-char (point-min))
                               (when (search-forward-regexp "^#" nil t)
                                 (goto-char (match-beginning 0)))
                               (buffer-substring-no-properties (point) (point-max))))
                   res)))
              res)))
  (setq-default gptel--system-message (alist-get 'default gptel-directives)))

Functions

New gptel
(defvar my-gptel:buffers-list '())

(defun my-gptel/new-buffer-name ()
  (let ((n (length my-gptel:buffers-list)))
    (cl-case n
      (0 "*Gptel*")
      (t (t! "*Gptel <<n>>*")))))

(defun my-gptel|new ()
  (interactive)
  (let ((selection (my-buffer/selected-text))
        (source-major-mode (s-chop-suffix "-mode" (symbol-name major-mode)))
        (buffer (-> (my-gptel/new-buffer-name)
                    (gptel))))
    (add-to-list 'my-gptel:buffers-list buffer)
    (pop-to-buffer buffer)
    (when selection
      (save-excursion
        (with-current-buffer buffer
          (insert (t! "

#+BEGIN_SRC <<source-major-mode>>
<<selection>>
#+END_SRC

")))))
    (goto-char (point-at-eol))
    (evil-insert-state t)))

(defun my-gpel|select-last-buffer ()
  (interactive)
  (when-let ((b (-last-item my-gptel:buffers-list)))
    (switch-to-buffer b)))

Astro

Config

(use-package! astro-mode
  :hook
  (astro-mode . lsp))

(set-formatter! 'prettier-astro
  '("prettier" "--parser=astro"
    (apheleia-formatters-indent "--use-tabs" "--tab-width" 'astro-ts-mode-indent-offset))
  :modes '(astro-mode))

Git / Magit

Packages

Magit & Transient

(package! magit :pin "7dfebba55bf687a25049882c2316166d968048ea")
(package! compat :pin "9a234d0")
(package! transient :pin "3430943eaa3222cd2a487d4c102ec51e10e7e3c9")
(package! forge :pin "a63685b")
(package! ghub :pin "23d1cd6")
Package
(package! elescope)
Config
(use-package elescope
  :commands (elescope-checkout)
  :config
  (setq elescope-root-folder my-directories:repositories-dir)
  :init
  (defalias '+git|clone 'elescope-checkout))
Package
(package! git-lens)
Config
(use-package! git-lens
  :commands (git-lens))
Config
(use-package! code-review
  :config
  (setq code-review-auth-login-marker 'forge))
Config
(use-package browse-at-remote
  :config
  (add-to-list
   'browse-at-remote-remote-type-regexps
   '(:host "^gitea\\.florianschroedl\\.com$" :type "gitea")))

Functions

Stash list

(defun my-magit/stash-list ()
  (mapcar (lambda (c)
            (pcase-let ((`(,rev ,msg) (split-string c "\0")))
              (list rev msg)))
          (magit-list-stashes "%gd%x00%s")))

(comment
 (->> (my-magit/stash-list)
      (-map (fn! (second %))))
 nil)

Create worktree workspace

Creates a new git workspace from a branch. Automatically adds .projectfile and opens a new doom workspace.

(defun my-magit|create-worktree-project-from-branch (branch start-point &optional force)
  "Create a new BRANCH and check it out in a new worktree at PATH in a new workspace."
  (interactive
   `(,@(magit-branch-read-args "Create and checkout branch")
     ,current-prefix-arg))
  (let* ((worktree-path (f-join (my-file/git-root) ".worktrees"))
         (path (f-join worktree-path branch)))
    (when (not (f-exists-p worktree-path))
      (mkdir worktree-path t))
    (magit-run-git "worktree" "add" (if force "-B" "-b") branch (expand-file-name path) start-point)
    (f-touch (f-join path ".projectile"))
    ;; Ignore existing workspace
    (condition-case nil
        (+workspace-new branch)
      (error nil))
    (+workspace-switch branch)
    (magit-diff-visit-directory path)
    (projectile-add-known-project path)
    path))
Transient binding
(after! magit
  (transient-append-suffix 'magit-worktree "b" '("c" "Create branch and worktree" my-magit|create-worktree-project-from-branch)))

Revision show original file

Show the original file when visiting a revision buffer. E.g.: When showing a diff from a commit, you may want to edit that file.

(defun my-magit|show-revision-original ()
  "Show the orginal file from a revision buffer
If possible also go to the pointing line"
  (interactive)
  (when magit-buffer-file-name
    (let ((file-name magit-buffer-file-name)
          (line-number (line-number-at-pos)))
      (if current-prefix-arg
          (delete-other-windows))
      (find-file file-name)
      (goto-line line-number))))

Changed files in branch

Show a list of the changed files in the current branch. For now only works on branches that were directly forked from master.

(defun my-magit/list-new-files ()
  "List of added files in the current branch."
  (my-shell/command-to-list "git ls-files -om --exclude-standard"))

(defun my-magit/list-modified-files (&optional branch)
  "Get a list of modified files from the BRANCH to head."
  (my-shell/command-to-list
   (t! "git --no-pager diff --no-renames --name-only --no-merges <<(magit-rev-parse \"HEAD\")>> <<branch>>;")))

(defun my-magit/list-branch-changed-files (branch)
  "Get a list of new and modified files from BRANCH to head."
  (->> (my-magit/list-modified-files branch)
       (-concat (my-magit/list-new-files))
       (-uniq)
       (-filter
        (lambda (x)
          (let ((default-directory (projectile-project-root)))
            (f-exists? x))))))

(defun my-magit|counsel-changed-files (&optional branch)
  (interactive)
  (let ((enable-recursive-minibuffers t))
    (ivy-read (template "Changed files for <<(or branch (magit-get-current-branch))>>:")
              (my-magit/list-branch-changed-files (or "origin/master"))
              :require-match t
              :history 'file-name-history
              :action counsel-projectile-find-file-action
              :caller 'my-magit|counsel-changed-files)))

Undo commit

(defun my-magit|git-undo ()
  "Soft reset current git repo to HEAD~1."
  (interactive)
  (magit-reset-soft "HEAD~1"))

Push dated remote branch

For work I need remote branches with a date prefix.

(defun my-magit|push-dated (&optional branch)
  "Pushes the given the current BRANCH with a dated prefix
my-branch-name -> 19-01-my-branch-name
When no BRANCH is given, take the current one."
  (interactive)
  (let* ((branch (or branch (magit-get-current-branch)))
         (date (format-time-string "%y-%m"))
         (remote (template "origin/<<date>>-<<branch>>")))
    (magit-git-push branch remote "--set-upstream")
    remote))

Diff range from current branch to magit-thing-at-point

When I’m on the log view, I want to quickly diff it against the currently checked out branch. The transient shortcut for this is d R define here.

(defun my-magit|diff-range-from-current-branch ()
  "Ranged diff from the checked out branch to the commit at point."
  (interactive)
  (magit-diff-range (template "<<(magit-commit-at-point)>>..<<(magit-get-current-branch)>>")))

Diff range from current pull request

(defun my-magit|diff-range-from-pullreq ()
  "Ranged diff from the pull request under point."
  (interactive)
  (-some->> (forge-current-topic)
    (forge--pullreq-range)
    (magit-diff-range)))

Review branch

(defun my-magit|delete-review-branches ()
  "Delete all review branches that no longer have an upstream."
  (interactive)
  (->> (magit-list-branches)
       (--filter (s-starts-with? "refs/heads/REVIEW" it))
       (--map (magit-name-local-branch it))
       (--reject (magit-get-upstream-branch it))
       (--each (lambda (x) (magit-branch-delete x t)))))

(defun my-magit|checkout-review-branches (&optional branch start-point)
  "Create a branch with review prefix for easy cleanup afterwards."
  (interactive)
  (let* ((remotes (magit-list-remote-branch-names))
         (atpoint (magit-branch-at-point))
         (branch (magit-completing-read
                  "Checkout branch" remotes
                  nil nil nil 'magit-revision-history
                  (or (car (member atpoint remotes))
                      (and atpoint
                           (car (member (and (string-match "[^/]+/" atpoint)
                                             (substring atpoint (match-end 0)))
                                        remotes))))))
         (review-branch-name (s-replace "origin/" "REVIEW-" branch)))
    ;; HACK Workaround where the buffer cant be read
    (advice-remove 'magit-checkout #'+magit-revert-repo-buffers-deferred-a)
    (magit-checkout branch)
    (when (magit-anything-modified-p)
      (user-error "Cannot checkout when there are uncommitted changes"))
    (if (-contains? (magit-list-local-branch-names) review-branch-name)
        (magit-branch-checkout review-branch-name)
      (magit-branch-and-checkout (s-replace "origin/" "REVIEW-" branch) branch))))

Cleanup branches

Cleans up all merged and review branches

(defun my-magit|cleanup-branches (&optional base-branch)
  "Remove all merged and review branches."
  (interactive)
  (my-magit|delete-review-branches)
  (let* ((base-branch (or base-branch "master")))
    (call-interactively #'my-magit|delete-review-branches)
    (deferred:$
      (deferred:process-shell (template "git branch --merged | egrep -v \"(^\\*|<<base-branch>>)\" | xargs git branch -d"))
      (deferred:nextc it
        (magit-status-maybe-update-revision-buffer)))))

Branches by user

(defun my-magit|branches-by-user (&optional ignore-review?)
  "List all branches by user.
Universal argument to ignore review branches."
  (interactive "P")
  (let ((branches
         (->>
          (concat "git for-each-ref"
                  " --sort=-committerdate"
                  " --format='%(committerdate) %(authorname) %(refname)'"
                  " --sort=-committerdate"
                  ;; refs/remotes/origin/ for remote branches
                  " refs/heads"
                  " | grep -e 'Florian Schroedl'")
          (shell-command-to-string)
          (s-split "\n")
          (-butlast)
          (-map (lambda (x) (->> (s-match ".*Florian Schroedl refs\\/heads\\/\\(.*\\)" x)
                                 (-last-item))))
          ((lambda (xs)
             (if ignore-review?
                 (--reject (s-starts-with? "REVIEW" it) xs)
               xs))))))
    (ivy-read "Checkout: " branches :action #'magit-checkout)))

Commit Template

Loads commit template from ./git/TEMPLATE if the file exist.

(defun my-magit::commit-template/find-template-file (branch)
  (when-let* ((dir (my-magit/find-root-git-dir))
              (entries (f-entries dir (lambda (x) (s-starts-with? "TEMPLATE" (f-base x)))))
              (branch-template (template "TEMPLATE-<<branch>>")))
    (or
     (-find (lambda (x) (string= branch-template (f-base x))) entries)
     (-find (lambda (x) (string= "TEMPLATE" (f-base x))) entries))))

(defun my-magit::commit-template|remove-template-file ()
  "Open an existing template file or create a new one."
  (interactive)
  (-when-let* ((file (my-magit::commit-template/find-template-file (magit-get-current-branch))))
    (when (save-window-excursion
            (find-file file)
            (y-or-n-p "Delete commit template file?"))
      (f-delete file))))

(defun my-magit::commit-template|edit-template-file ()
  "Open an existing template file or create a new one."
  (interactive)
  (let ((file (or (my-magit::commit-template/find-template-file (magit-get-current-branch))
                  (f-join (my-magit/find-root-git-dir) "TEMPLATE"))))
    (find-file file)))

(defun my-magit::commit-template/insert-template (&rest _)
  "When a TEMPLATE file in the git directory exists, insert it as a commit template."
  (when (eq (point) (point-at-eol))
    (or
     (-some->> (my-magit::commit-template/find-template-file (magit-get-current-branch))
               (f-read)
               (s-trim)
               (s-append " ")
               (insert))
     (when-let ((emoji (my-gitmoji/prev-commit-gitmoji)))
       (insert emoji " "))))
  (evil-insert-state))

And the hook

(add-hook! 'git-commit-setup-hook :append #'my-magit::commit-template/insert-template)

Find root directory

Find the root directory of a .git repository This also works for worktrees that are in a nested directory.

(defun my-magit/find-root-git-dir ()
  "Find the root directory of a repository."
    (-some->> (magit-toplevel)
      (-f-join ".git")
      (-id-when #'f-exists?)
      ((lambda (x)
         (if (f-file? x)
             (->> (magit-list-worktrees)
                  (car)
                  (car)
                  (-f-join ".git"))
           x)))))

Lock git buffer

(advice-add #'magit-toggle-buffer-lock :after (lambda () (my/bool-state-message 'magit-buffer-locked-p)))

Check for merge conflicts

Try two merge two branches in memory, to determine if there would be merge conflicts.

(defun my-magit/check-for-merge-conflicts (&optional source-branch target-branch)
  "Try to merge SOURCE-BRANCH into TARGET-BRANCH, return list of conflicting files when there are any.
This will try to merge in memory, so no index files will be created.
SOURCE-BRANCH defaults to the current branch.
TARGET-BRANCH defaults to origin/master."
  (interactive)
  (let* ((source-branch (or source-branch (magit-get-current-branch)))
         (target-branch (or target-branch "origin/master"))
         (merge-base (my-shell/command-to-list (t! "git merge-base <<target-branch>> <<source-branch>>"))))
    (my-shell/command-to-list (t! "git merge-tree <<(car merge-base)>> <<target-branch>> <<source-branch>>"))))

Copy over changes from diff

(defun my-magit|diff-copy-file-changes ()
  "Copies over contents of diff at point to the curent file system."
  (interactive)
  (save-excursion
    (let ((buffer (call-interactively #'magit-diff-visit-file)))
      (with-current-buffer buffer
        (make-directory (f-dirname magit-buffer-file-name) t)
        (f-write (substring-no-properties (buffer-string)) 'utf-8 magit-buffer-file-name)))
    (kill-buffer)))

Smerge mode hydra

(use-package smerge-mode
  :after hydra
  :config
  (defhydra unpackaged/smerge-hydra
    (:color pink :hint nil :post (smerge-auto-leave))
    "
^Move^        ^Keep^                ^Diff^                  ^Other^
^^------------^^---------------------^^----------------------^^-------
_gn_ext       _gb_ase               _g<_: upper/base        _gC_ombine
_gp_rev       _gu_pper              _g=_: upper/lower       _gr_esolve
^^            _gl_ower              _g>_: base/lower        _gk_ill current
^^            _ga_ll                _gR_efine
^^                                  _gE_diff
"
    ("gn" smerge-next)
    ("gp" smerge-prev)
    ("gb" smerge-keep-base)
    ("gu" smerge-keep-upper)
    ("gl" smerge-keep-lower)
    ("ga" smerge-keep-all)
    ("g<" smerge-diff-base-upper)
    ("g=" smerge-diff-upper-lower)
    ("g>" smerge-diff-base-lower)
    ("gR" smerge-refine)
    ("gE" smerge-ediff)
    ("gC" smerge-combine-with-next)
    ("gr" smerge-resolve)
    ("gk" smerge-kill-current)
    ("ZZ" (lambda ()
            (interactive)
            (save-buffer)
            (bury-buffer))
     "Save and bury buffer" :color blue)
    ("q" nil "cancel" :color blue))
  (add-hook! 'magit-diff-visit-file-hook
    (defun my-smerge-mode*init-smerge-mode ()
      "Reveal the point if in an invisible region."
      (when smerge-mode
        ;; Prevent parinfer triggering on diff files
        (when parinfer-rust-mode
          (parinfer-rust-mode -1)
          (parinfer-rust-mode-disable))
        ;; Disable flycheck for diff buffers
        (flycheck-mode -1)
        (unpackaged/smerge-hydra/body)))))

Merge master

(defun my-magit|merge-master ()
  "Merges origin master to the current branch."
  (interactive)
  (magit-merge-plain "origin/master"))

Git related find file

(require 'cl-lib)
(require 'subr-x)
(require 'project)
(require 'vc-git)

(defface git-related-score
  '((t (:foreground "#50a6c6")))
  "Face used for git related score."
  :group 'git-related)

(defface git-related-file
  '((t (:foreground "#82898b")))
  "Face used for git related file name."
  :group 'git-related)

(defvar git-related--graphs nil)

(cl-defstruct git-related--graph files commits)
(cl-defstruct git-related--file (name "" :type string) (commits nil :type list))
(cl-defstruct git-related--commit (sha "" :type string) (files nil :type list))

(defun git-related--new-graph ()
  "Create an empty graph."
  (make-git-related--graph
   :files (make-hash-table :test 'equal :size 2500)
   :commits (make-hash-table :test 'equal :size 2500)))

(defun git-related--record-commit (graph sha filenames)
  "Record in the GRAPH the relation between SHA and FILENAMES."
  (let ((commit (make-git-related--commit :sha sha)))
    (dolist (filename filenames)
      (let* ((seen-file (gethash filename (git-related--graph-files graph)))
             (file-found (not (null seen-file)))
             (file (or seen-file (make-git-related--file :name filename))))

        (cl-pushnew commit (git-related--file-commits file))
        (cl-pushnew file (git-related--commit-files commit))

        (unless file-found
          (setf (gethash filename (git-related--graph-files graph)) file))))

    (setf (gethash sha (git-related--graph-commits graph)) commit)))

(defun git-related--replay (&optional graph)
  "Replay git commit history into optional GRAPH."
  (let ((graph (or graph (git-related--new-graph))))
    (with-temp-buffer
      (process-file vc-git-program nil t nil "log" "--name-only" "--format=%x00%H")
      (let* ((commits (split-string (buffer-string) "\0" t))
             (replay-count 0)
             (progress-reporter (make-progress-reporter "Building commit-file graph..." 0 (length commits))))
        (dolist (commit commits)
          (let* ((sha-and-paths (split-string commit "\n\n" t (rx whitespace)))
                 (sha (car sha-and-paths))
                 (paths (when (cadr sha-and-paths)
                          (split-string (cadr sha-and-paths) "\n" t (rx whitespace)))))
            (git-related--record-commit graph sha paths)
            (progress-reporter-update progress-reporter (cl-incf replay-count))))
        (progress-reporter-done progress-reporter)))
    graph))

(defun git-related--similar-files (graph filename)
  "Return files in GRAPH that are similar to FILENAME."
  (unless (git-related--graph-p graph)
    (user-error "You need to index this project first"))
  (let ((file (gethash filename (git-related--graph-files graph))))
    (when file
      (let ((file-sqrt (sqrt (length (git-related--file-commits file))))
            (neighbor-sqrts (make-hash-table :test 'equal :size 100))
            (hits (make-hash-table :test 'equal :size 100)))

        (dolist (commit (git-related--file-commits file))
          (dolist (neighbor (remove file (git-related--commit-files commit)))
            (let ((count (cl-incf (gethash (git-related--file-name neighbor) hits 0))))
              (when (= count 1)
                (setf (gethash (git-related--file-name neighbor) neighbor-sqrts)
                      (sqrt (length (git-related--file-commits neighbor))))))))

        (let (ranked-neighbors)
          (maphash
           (lambda (neighbor-name neighbor-sqrt)
             (let ((axb (* file-sqrt neighbor-sqrt))
                   (n (gethash neighbor-name hits)))
               (push (list (if (cl-plusp axb) (/ n axb) 0.0) neighbor-name) ranked-neighbors)))
           neighbor-sqrts)
          (cl-sort
           (cl-remove-if-not #'git-related--file-exists-p ranked-neighbors :key #'cadr)
           #'> :key #'car))))))

(defun git-related--file-exists-p (relative-filename)
  "Determine if RELATIVE-FILENAME currently exists."
  (file-exists-p
   (expand-file-name relative-filename
                     (my-file/git-root nil))))

(defun git-related--propertize (hit)
  "Given the cons HIT return a rendered representation for completion."
  (propertize
   (concat
    (propertize (format "%2.2f" (car hit)) 'face 'git-related-score)
    " ---> "
    (propertize (cadr hit) 'face 'git-related-file))
   'path (cadr hit)))

;;;###autoload
(defun git-related-update ()
  "Update graph for the current project."
  (interactive)
  (let* ((default-directory (my-file/git-root nil))
         (project-symbol (intern (project-name (project-current))))
         (graph (cl-getf git-related--graphs project-symbol)))
    (setf (cl-getf git-related--graphs project-symbol)
          (git-related--replay graph))))

;;;###autoload
(defun git-related-find-file ()
  "Find files related through commit history."
  (interactive)
  (if (buffer-file-name)
      (let ((default-directory (my-file/git-root nil)))
        (find-file
         (let* ((selection (completing-read "Related files: "
                                            (mapcar #'git-related--propertize
                                                    (git-related--similar-files
                                                     (cl-getf git-related--graphs (intern (project-name (project-current))))
                                                     (file-relative-name (buffer-file-name) (my-file/git-root nil))))
                                            nil t)))
           (when selection
             (let ((filename (get-text-property 0 'path selection)))
               (find-file filename))))))
    (message "Current buffer has no file")))

(provide 'git-related)

;;; git-related.el ends here

Delete multiple worktrees

(defun my-magit|worktree-delete (worktrees)
  "Delete one or multiple worktrees."
  (interactive
   (list (ivy-read "Delete worktree" (cdr (magit-list-worktrees))
                   :action (lambda (x)
                             (magit-worktree-delete (first x)))
                   :multi-action (lambda (xs)
                                   (--map (magit-worktree-delete (first it)) xs))))))

Difftastic

Source: Difftastic diffing with Magit

(defun th/magit--with-difftastic (buffer command)
  "Run COMMAND with GIT_EXTERNAL_DIFF=difft then show result in BUFFER."
  (let ((process-environment
         (cons (concat "GIT_EXTERNAL_DIFF=difft --width="
                       (number-to-string (frame-width)))
               process-environment)))
    ;; Clear the result buffer (we might regenerate a diff, e.g., for
    ;; the current changes in our working directory).
    (with-current-buffer buffer
      (setq buffer-read-only nil)
      (erase-buffer))
    ;; Now spawn a process calling the git COMMAND.
    (make-process
     :name (buffer-name buffer)
     :buffer buffer
     :command command
     ;; Don't query for running processes when emacs is quit.
     :noquery t
     ;; Show the result buffer once the process has finished.
     :sentinel (lambda (proc event)
                 (when (eq (process-status proc) 'exit)
                   (with-current-buffer (process-buffer proc)
                     (goto-char (point-min))
                     (ansi-color-apply-on-region (point-min) (point-max))
                     (setq buffer-read-only t)
                     (view-mode)
                     (end-of-line)
                     ;; difftastic diffs are usually 2-column side-by-side,
                     ;; so ensure our window is wide enough.
                     (let ((width (current-column)))
                       (while (zerop (forward-line 1))
                         (end-of-line)
                         (setq width (max (current-column) width)))
                       ;; Add column size of fringes
                       (setq width (+ width
                                      (fringe-columns 'left)
                                      (fringe-columns 'right)))
                       (goto-char (point-min))
                       (pop-to-buffer
                        (current-buffer)
                        `(;; If the buffer is that wide that splitting the frame in
                          ;; two side-by-side windows would result in less than
                          ;; 80 columns left, ensure it's shown at the bottom.
                          ,(when (> 80 (- (frame-width) width))
                             #'display-buffer-at-bottom)
                          (window-width
                           . ,(min width (frame-width))))))))))))

(defun th/magit-show-with-difftastic (rev)
  "Show the result of \"git show REV\" with GIT_EXTERNAL_DIFF=difft."
  (interactive
   (list (or
          ;; If REV is given, just use it.
          (when (boundp 'rev) rev)
          ;; If not invoked with prefix arg, try to guess the REV from
          ;; point's position.
          (and (not current-prefix-arg)
               (or (magit-thing-at-point 'git-revision t)
                   (magit-branch-or-commit-at-point)))
          ;; Otherwise, query the user.
          (magit-read-branch-or-commit "Revision"))))
  (if (not rev)
      (error "No revision specified")
    (th/magit--with-difftastic
     (get-buffer-create (concat "*git show difftastic " rev "*"))
     (list "git" "--no-pager" "show" "--ext-diff" rev))))

(defun th/magit-diff-with-difftastic (arg)
  "Show the result of \"git diff ARG\" with GIT_EXTERNAL_DIFF=difft."
  (interactive
   (list (or
          ;; If RANGE is given, just use it.
          (when (boundp 'range) range)
          ;; If prefix arg is given, query the user.
          (and current-prefix-arg
               (magit-diff-read-range-or-commit "Range"))
          ;; Otherwise, auto-guess based on position of point, e.g., based on
          ;; if we are in the Staged or Unstaged section.
          (pcase (magit-diff--dwim)
            ('unmerged (error "unmerged is not yet implemented"))
            ('unstaged nil)
            ('staged "--cached")
            (`(stash . ,value) (error "stash is not yet implemented"))
            (`(commit . ,value) (format "%s^..%s" value value))
            ((and range (pred stringp)) range)
            (_ (magit-diff-read-range-or-commit "Range/Commit"))))))
  (let ((name (concat "*git diff difftastic"
                      (if arg (concat " " arg) "")
                      "*")))
    (th/magit--with-difftastic
     (get-buffer-create name)
     `("git" "--no-pager" "diff" "--ext-diff" ,@(when arg (list arg))))))

(transient-define-prefix th/magit-aux-commands ()
  "My personal auxiliary magit commands."
  ["Auxiliary commands"
   ("d" "Difftastic Diff (dwim)" th/magit-diff-with-difftastic)
   ("s" "Difftastic Show" th/magit-show-with-difftastic)])

(transient-append-suffix 'magit-dispatch "!"
  '("#" "My Magit Cmds" th/magit-aux-commands))

(define-key magit-status-mode-map (kbd "#") #'th/magit-aux-commands)

Discard staget logs

(defvar my-magit|discard-staged-logs-regexp
  "^\\+.*\\(println\\|print\\|prn\\|print!\\|js\\/console.log\\|console.log\\)")

(comment
 (string-match my-magit|discard-staged-logs-regexp "- anything console.log after")
 (string-match my-magit|discard-staged-logs-regexp "+ anything console.log after")
 (string-match my-magit|discard-staged-logs-regexp "+ console.log")
 nil)


(defun my-magit|discard-staged-logs ()
  (interactive)
  (when-let ((log-lines (save-window-excursion
                          (with-current-buffer (magit-diff-staged)
                            (->> (buffer-substring-no-properties (point-min) (point-max))
                                 (s-lines)
                                 (--filter (s-matches? my-magit|discard-staged-logs-regexp it)))))))
    (-each log-lines
           (lambda (_)
             (with-current-buffer (magit-diff-staged)
               (evil-normal-state)
               (magit-refresh)
               (re-search-forward my-magit|discard-staged-logs-regexp nil t)
               (goto-char (point-at-bol))
               (evil-visual-line)
               (magit-discard)
               (kill-buffer))))))

Config

(after! magit
  :config
  (setq
   magit-save-repository-buffers 'dontask
   magit-clone-default-directory "~/Code/Repositories/"
   magithub-clone-default-directory magit-clone-default-directory
   git-commit-summary-max-length 120))

(after! forge
  :config
  (setq forge-database-file (f-join doom-local-dir "forge-database.sqlite")))

Fix smerge mode color

(after! smerge-mode
  :config
  ;; TODO This is broken after switching the theme but works for now
  ;; This fixes the smerge diff color is really bright an ugly
  (set-face-attribute 'smerge-refined-added nil :foreground nil :background nil))

magit-find-file customizations

magit-blob-mode overrides loads of evil bindings, making it very annoying to use. The mode doesn’t really bring big advantages so I remove the hook.

(after! magit
  ;; Fix binding overrides with evil
  (remove-hook 'magit-find-file-hook #'magit-blob-mode)
  ;; Add line-numbers
  (add-hook! 'magit-find-file-hook #'display-line-numbers-mode))

Bindings

Diff Navigation

My workflow for navigating diffs Use z1 to fold all diffs to their file headers and press’s { or } to

  1. Refold all sections
  2. Go to the next section
  3. Unfold everything in the current section

Then use ] to navigate the sections

(defun my-magit/jumpunfold-section (&optional forward)
  "Fold all section. Go to next section when FORWARD. Show all children"
  (interactive)
  (magit-section-show-level-1-all)
  (call-interactively (if forward #'magit-section-forward-sibling #'magit-section-backward-sibling))
  (call-interactively #'magit-section-show-children))

(map!
 (:map magit-diff-mode-map
  :nv "}" (cmd! (my-magit/jumpunfold-section 't))
  :nv "{" (cmd! (my-magit/jumpunfold-section))))

Code Review

(map!
 :after code-review
 :map code-review-mode-map
 :ng "RET" #'magit-diff-visit-file
 :ng "TAB" #'magit-section-toggle
 :ng "<tab>" #'magit-section-toggle
 :n "[[" #'code-review-comment-jump-previous
 :n "]]" #'code-review-comment-jump-next)

Time Machine Navigation

(map!
 :after git-timemachine
 :map git-timemachine-mode-map
 :n "[" #'git-timemachine-show-previous-revision
 :n "]" #'git-timemachine-show-next-revision
 :n "gb" #'git-timemachine-blame)

Disable quit for locked buffers

Disable accidentally quitting magit buffers with q when the buffer is locked.

(defun my-magit/disable-locked-quit (orig-fn &rest args)
  (unless magit-buffer-locked-p
    (apply orig-fn args)))

(advice-add #'magit-mode-bury-buffer :around #'my-magit/disable-locked-quit)

Fix text mode

Restores functionality when editing magit buffers as text. For now only the toggle functionality is needed.

(defvar +magit-evil-edit-mode-map (make-sparse-keymap))

(define-minor-mode +magit-evil-edit-mode ""
  :keymap +magit-evil-edit-mode-map)

(map! :map +magit-evil-edit-mode-map
      :n [tab] #'magit-section-toggle)

Transient Bindings

You can add flags or commands to the magit interface transient here. To append something, just state the flag that you see in the transient popup as the 2nd argument.

(after! magit
  (transient-append-suffix 'magit-log "-f" '("-0" "No merges" "--no-merges"))
  (transient-append-suffix 'magit-push "p" '("d" "dated" my-magit|push-dated))

  (transient-append-suffix 'magit-diff "d" '("R" "Diff range from current branch" my-magit|diff-range-from-current-branch))
  (transient-append-suffix 'magit-diff "d" '("P" "Pullrequest Range" my-magit|diff-range-from-pullreq))
  (transient-append-suffix 'magit-diff "d" '("f" "File" magit-diff-buffer-file))

  (transient-append-suffix 'magit-commit
    "-R" '("-D" "Override the author date" "--date=" transient-read-date))
  (transient-append-suffix 'magit-merge
    "m" '("M" "Merge master" my-magit|merge-master))

  (transient-append-suffix 'magit-branch "l" '("R" "Create review branch" my-magit|checkout-review-branches))
  (transient-append-suffix 'magit-branch "l" '("U" "My Branches" my-magit|branches-by-user)))

Dired

Packages

Filter current dired buffer by query/extension etc. Trigger with g/. in normal-mode.

Package
(package! dired-filter)
Config
(use-package! dired-filter
  :after dired
  :config
  (setq dired-filter-saved-filters
        (quote (("images"
                 (extension "jpg" "png" "gif"))
                ("media"
                 (extension "mp3" "mp4" "MP3" "MP4" "avi" "mpg" "flv" "ogg" "wmv" "mkv" "mov" "wma"))
                ("archives"
                 (extension "zip" "bz2" "tgz" "txz" "gz" "xz" "z" "Z" "jar" "war" "ear" "rar" "sar" "xpi" "apk" "xz" "tar"))
                ("documents"
                 (extension "doc" "docx" "odt" "pdb" "pdf" "ps" "rtf" "djvu" "epub")))))
  (dired-filter-define created-today
      "Show only files that are newer than today."
    (:description "created-today")
    (let ((case-fold-search nil))
      (time-less-p (time-subtract (current-time) (* 60 60 24))
                   (file-attribute-modification-time (file-attributes file-name))))))
Config
(use-package! dired-subtree
  :after dired
  :config
  (evil-define-key 'normal dired-mode-map
    (kbd "<tab>") (cmd! () (dired-subtree-toggle)
                      ;; Fix for dired-all-the-icons not showing up
                      (dired-revert))
    (kbd "<backtab>") (cmd! () (dired-subtree-cycle)
                          (dired-revert))
    (kbd "gh") 'dired-subtree-up))
(package! dired-subtree)

Config

Listing Switches / Sorting

(defconst my-dired:default-listing-switches
  "-ahl -t --group-directories-first")

(setq dired-listing-switches my-dired:default-listing-switches)

Omit files types in dired

(use-package! dired
  :config
  (setq dired-omit-files
        (concat dired-omit-files
                ;; Apple files
                "\\|\\.com.apple.timemachine.donotpresent$"
                "\\|\\.Spotlight-V100$"
                "\\|\\.fseventsd$"
                "\\|\\.DocumentRevisions-V100$"
                "\\|\\.TemporaryItems$"
                ;; Windows files
                "\\|\\System Volume Information$")))

Automatically revert dired buffers

(add-hook 'dired-mode-hook 'auto-revert-mode)

Automatically create directories when moving/copying items

(after! dired
  (defadvice dired-mark-read-file-name (after rv:dired-create-dir-when-needed (prompt dir op-symbol arg files &optional default) activate)
    (when (member op-symbol '(copy move))
      (let ((directory-name (if (< 1 (length files))
                                ad-return-value
                              (file-name-directory ad-return-value))))
        (when (and (not (file-directory-p directory-name))
                   (y-or-n-p (format "directory %s doesn't exist, create it?" directory-name)))
          (make-directory directory-name t))))))

Enable Async Mode

(after! async
  (dired-async-mode 1))

Functions

Kill all dired buffers

(defun my-dired|kill-dired-buffers ()
  "Kills all dired buffers."
  (interactive)
  (mapc (lambda (buffer)
          (when (eq 'dired-mode (buffer-local-value 'major-mode buffer))
            (kill-buffer buffer)))
        (buffer-list)))

Show photos

(defun my-dired|photos-viewer ()
  (interactive)
  (my-shell/async-command-no-window  "find . -type f -iregex '.*\.\(jpg\|jpeg\|png\|gif\|JPG\|JPEG\|PNG\|GIF\|orf\|raw\|ORF\|RAW\)' -print0 | xargs -0 geeqie"))

Go to mounted drive

(defun my-dired|find-mounted-drive ()
  "Browse mounted disk.
When the mounted drive has only 1 partition, select this one otherwise show listing of disks."
  (interactive)
  (let ((dir (f-join "/run/media/" user-login-name)))
    (counsel-find-file
     (-some->> dir
       (f-directories)
       (--id-when (eq 1 (length it)))
       (car))
     (f-join "/run/media/" user-login-name))))

Unparent directory at point

(defun my-dired|ungroup-directory ()
  "Move all files and directories to the current directory,
If everything goes well and the directory is empty, safely delete it.
It might not work out though, when any of the entries has the same name,
in that case throw a warning."
  (interactive)
  (->> (dired-get-marked-files)
       (-map (lambda (x)
               (let ((cmd (if (my-file/dir-has-hidden-entries x)
                              "mv -n ./* ./.* ../"
                            "mv -n ./* ../")))
                 (shell-command (t! "cd \"<<x>>\"; <<cmd>>"))
                 (when (f-empty? x)
                   (dired-delete-file x))))))
  (revert-buffer))

Mouse navigation of the heading directory

Allows to click on the header bar to got to navigate tree breadcrumbs in the dired header bar.

(defface dired-mouseover-face
  '((t (:foreground "green")))
  "Face for `dired-mouseover-face'."
  :group 'dired)

(defvar dired-mouse-map
  (let ((map (make-sparse-keymap)))
    (define-key map [mouse-2] 'dired-follow-link)
    (define-key map [return] 'dired-follow-link)
    (define-key map [follow-link] 'mouse-face)
    map)
  "Keymap for mouse when in `dired-mode'.")

;; Author:  Drew Adams -- http://emacs.stackexchange.com/a/13411/2287
(defun dired-follow-link (event)
  "Follow the link in the dired directory heading, causing a new
dired buffer to be opened.
When clicking on the current directory always go one directory upwards."
  (interactive (list last-nonmenu-event))
  (run-hooks 'mouse-leave-buffer-hook)
  (with-current-buffer (window-buffer (posn-window (event-start event)))
    (let ((path  (get-text-property (posn-point (event-start event)) 'breadcrumb)))
      (if (f-equal? dired-directory path)
          (dired-up-directory)
        (dired path)))))

(defun dired-propertize-directory-heading ()
  (interactive)
  (unless (buffer-narrowed-p)
    (let* (
           p beg end path peol
           (inhibit-read-only t))
      (save-excursion
        (goto-char (point-min))
        (setq peol (point-at-eol))
        (set-text-properties (point) peol nil)
        (re-search-forward "\\([^/\\]+\\)[/\\]" peol t)
        (when (looking-back "\\(^ +\\)\\([a-zA-Z]:\\)?/")
          (setq p (match-end 1))
          (setq path (if (match-string 2) (concat (match-string 2) "/") "/"))
          (add-text-properties (point-min) (1- (match-end 0)) (list
                                                               'breadcrumb path
                                                               'mouse-face 'dired-mouseover-face
                                                               'help-echo (format "mouse-2, RET: Follow the link to \"%s\"." path)
                                                               'keymap dired-mouse-map)))
        (while (re-search-forward "\\([^/\\]+\\)[/\\]" peol t)
          (setq beg (match-beginning 1))
          (setq end (match-end 1))
          (setq path (buffer-substring-no-properties p end))
          (add-text-properties beg end (list
                                        'breadcrumb path
                                        'mouse-face 'dired-mouseover-face
                                        'help-echo (format "mouse-2, RET: Follow the link to \"%s\"." path)
                                        'keymap dired-mouse-map)))
        (setq path (buffer-substring-no-properties p (1- peol)))
        (add-text-properties (point) (1- peol) (list
                                                'breadcrumb path
                                                'mouse-face 'dired-mouseover-face
                                                'help-echo (format "mouse-2, RET: Follow the link to \"%s\"." path)
                                                'keymap dired-mouse-map))))))

(add-hook 'dired-after-readin-hook 'dired-propertize-directory-heading)

Get marked file size

(defun my-dired|get-marked-file-size ()
  "Get the file size of all marked dired entries."
  (interactive)
  (let ((files (dired-get-marked-files)))
    (with-temp-buffer
      (apply 'call-process "du" nil t nil "-sch" files)
      (message
       "Size of all marked files: %s"
       (progn (re-search-backward "\\(^[0-9.,]+[A-Za-z]+\\).*total$") (match-string 1))))))

Toggle sorting

When you toggle the sorting via dired-sort-toggle-or-edit the cursor stays at the current file, which is very disorienting. With this function the cursor stays on the current line. save-excursion does not work in this function, it just throws to the top of the buffer. The dired-line jumps at the end are used to jump the filename again.

(defun my-dired|toggle-sorting (&optional arg)
  "Change sorting but stay on the same line."
  (interactive "P")
  (let ((pos (point)))
    (cond
     ((eq arg 3)
      (setq dired-listing-switches (symbol-value 'my-dired:default-listing-switches))
      (setq dired-actual-switches (symbol-value 'my-dired:default-listing-switches))
      (dired-sort-set-mode-line)
      (revert-buffer))
     ((eq arg 2)
      (setq dired-listing-switches
            (read-string "Global ls switches (must contain -l): " dired-listing-switches))
      (setq dired-actual-switches (symbol-value 'dired-listing-switches))
      (dired-sort-set-mode-line)
      (revert-buffer))
     (arg
      (setq dired-actual-switches
            (read-string "Buffer ls switches (must contain -l): " dired-actual-switches)))
     (t (dired-sort-toggle-or-edit)))
    (goto-char pos)
    (dired-next-line 1)
    (dired-previous-line 1)))

Open file externally

Config
(defvar my-dired::open-external:extensions nil
  "Which files to open externally by default when pressing enter.")

(setq my-dired::open-external:extensions
      '(
        ;; Video
        "mov" "mpv" "mkv" "webm" "avi"
        ;; Audio
        "mp4" "mp3" "ogg" "wav" "flac" "m4a"))
Function
(defun my-dired::open-external/mouse-open (event)
  "Open marked dired file(s) at point with an external application. Open directories normally"
  (interactive "e")
  (my-dired::open-external/mouse-open event))

(defun my-dired::open-external/mouse-open (event &optional find-file-func find-dir-func)
  "In Dired, visit the file or directory name you click on.
The optional arguments FIND-FILE-FUNC and FIND-DIR-FUNC specify
functions to visit the file and directory, respectively.  If
omitted or nil, these arguments default to `find-file' and `dired',
respectively."
  (interactive "e")
  (or find-file-func (setq find-file-func 'my-dired::open-external/xdg-open))
  (or find-dir-func (setq find-dir-func 'dired))
  (let (window pos file)
    (save-excursion
      (setq window (posn-window (event-end event))
            pos (posn-point (event-end event)))
      (if (not (windowp window))
          (error "No file chosen"))
      (set-buffer (window-buffer window))
      (goto-char pos)
      (setq file (dired-get-file-for-visit))
      (if (file-directory-p file)
          (or (and (cdr dired-subdir-alist)
                   (dired-goto-subdir file))
              (progn
                (select-window window (funcall find-dir-func file))))
        (select-window window)
        (funcall find-file-func)))))

(defun my-dired::open-external/xdg-open (&optional file)
  "Open marked dired file(s) at point with an external application."
  (let ((file-list (or (list file) (dired-get-marked-files)))
        (process-connection-type nil))
    (--map (my-shell/no-exit-command "xdg-open" (s-wrap (expand-file-name it) "\"")) file-list)))

(defun my-dired::open-external|open-dwim (&optional file)
  (interactive)
  (let ((file-list (or file (dired-get-marked-files)))
        (process-connection-type nil))
    (-each file-list (lambda (x)
                       (message "%s" x)
                       (let* ((path (expand-file-name x))
                              (ext (-some-> (f-ext path)
                                            (s-downcase))))
                         (if (-contains? my-dired::open-external:extensions ext)
                             (my-shell/no-exit-command "xdg-open" (s-wrap path "\""))
                           (dired-find-file)))))))

Convert Images

(defun my-dired|convert-image (&optional arg)
  "Convert image files to other formats."
  (interactive "P")
  (assert (executable-find "convert") nil "Install imagemagick")
  (let* ((dst-fpath)
         (src-fpath)
         (src-ext)
         (last-ext)
         (dst-ext))
    (mapc
     (lambda (fpath)
       (setq src-fpath fpath)
       (setq src-ext (downcase (file-name-extension src-fpath)))
       (when (or (null dst-ext)
                 (not (string-equal dst-ext last-ext)))
         (setq dst-ext (completing-read "to format: "
                                        (seq-remove (lambda (format)
                                                      (string-equal format src-ext))
                                                    '("jpg" "png")))))
       (setq last-ext dst-ext)
       (setq dst-fpath (format "%s.%s" (file-name-sans-extension src-fpath) dst-ext))
       (message "convert %s to %s ..." (file-name-nondirectory dst-fpath) dst-ext)
       (set-process-sentinel
        (start-process "convert"
                         (generate-new-buffer (format "*convert %s*" (file-name-nondirectory src-fpath)))
                         "convert" src-fpath dst-fpath)
        (lambda (process state)
          (if (= (process-exit-status process) 0)
              (message "convert %s" (file-name-nondirectory dst-fpath))
            (message "convert %s" (file-name-nondirectory dst-fpath))
            (message (with-current-buffer (process-buffer process)
                       (buffer-string))))
          (kill-buffer (process-buffer process)))))
     (dired-map-over-marks (dired-get-filename) arg))))

Shred File

(defun my-dired|do-shred ()
  "Run shred on marked files.
This will erase them."
  (interactive)
  (yes-or-no-p "Do you REALLY want to shred these files forever? ")
  (save-window-excursion
    (dired-do-async-shell-command "shred -zu" nil (dired-get-marked-files))))

Bindings

(map! :after dired
      :map dired-mode-map
      ;; Mouse controls
      :ng (kbd "<mouse-2>") 'my-dired::open-external|open-dwim
      :ng (kbd "<mouse-8>") 'dired-up-directory


      :n "q" (cmd! (kill-buffer))
      :n "Q" #'my-dired|kill-dired-buffers

      :ng "RET" 'my-dired::open-external|open-dwim
      :gn "o" 'my-dired|toggle-sorting
      :n "gg" 'my-dired|goto-top
      :localleader
      (:prefix ("s" . "search")
       "i"  #'dired-goto-subdir))

(define-key dired-mode-map (kbd "<down-mouse-1>") 'dired-mouse-find-file)

Go to top gg evil fix

When I do gg in dired, I mostly want to go to the first entry. This function first goes to the first entry and then to the top of the buffer.

(defun my-dired|goto-top ()
  "Go to first directory, when already there go to first line."
  (interactive)
  (if (<= (line-number-at-pos) 3)
      (goto-char (point-min))
    (goto-line 3)
    (dired-next-dirline 1)
    (dired-prev-dirline 1)))

Wdired mode evil fix

Stay in normal mode when switching to wdired

(defun my-dired|evil-wdired-mode ()
  "Stay in normal mode when switching to wdired."
  (interactive)
  (wdired-change-to-wdired-mode)
  (evil-normal-state))

(map! :after dired
      :map dired-mode-map
      :n "\\" #'my-dired|evil-wdired-mode)

Use same window for copying/renaming with prefix

I’ve set dired-dwim-target to t, so it uses the other window as the target destination. I undo this option with the interactive prefix argument, which can be accessed via SPC ucopy R/U.

(defun my-dired/dired-target-from-prefix (fn)
  (let ((dired-dwim-target (if (eq (prefix-numeric-value current-prefix-arg) 4) ;; Single C-u
                              nil
                            dired-dwim-target)))
      (call-interactively fn)))

(map! :after dired
      :map dired-mode-map
      :n "R" (cmd! (my-dired/dired-target-from-prefix #'dired-do-rename))
      :n "C" (cmd! (my-dired/dired-target-from-prefix #'dired-do-copy)))

Dired Paste DWIM

(defun my-dired|paste-dwim ()
  "Paste data in the current directory."
  (interactive)
  (let ((file (read-string "Filename: "))
        (last-clip-type (->> (shell-command-to-string "greenclip print")
                             (s-split "\n")
                             (-first-item))))
    (unless (string= "" file)
      (cond
       ((s-matches? "^image\\/png" last-clip-type)
        (shell-command-to-string (template "xclip -selection clipboard -t image/png -o > <<file>>")))))
    (dired-revert)))

(map! :after dired
      :map dired-mode-map
      :n "p" 'my-dired|paste-dwim)

System

Packages

Explore disk usage in emacs

Package
(package! disk-usage)
Config
(use-package! disk-usage
  :commands (disk-usage)
  :config
  (map! :map disk-usage-mode-map
        :n "-" #'disk-usage-up))

Edit browser input fields with emacs.

Package
(package! atomic-chrome)
Config
(use-package! atomic-chrome
  :commands (atomic-chrome-start-server)
  :config
  (setq
   atomic-chrome-url-major-mode-alist
   '(("github\\.com" . gfm-mode))))

Manage systemd from emacs

Package
(package! daemons)
Config
(use-package! daemons
  :commands (daemons))
Packages
(package! proced-narrow)
Functions
Shorten nixos path names in proced

Nixos paths can be very long, which is distracting in proced. This shortens the nixos paths to {nix}.

(defun my-proced/remove-nixos-path-name (oldformat &rest xs)
  (let ((xs (--map (->> it
                        (s-replace-regexp "/nix/store/[^/]+" "{nix}")
                        (s-replace-regexp (concat "^/home/" (user-login-name)) "~")
                        (s-replace-regexp (concat "^/etc/profiles/per-user/" (user-login-name)) "~")
                        ((lambda (x) (if (s-contains? "chromium" x) "{chromium}" x))))
                  xs)))
    (apply oldformat xs)))

(advice-add #'proced-format-args :around #'my-proced/remove-nixos-path-name)
Bindings
(map! :map proced-mode-map
      :n "/" #'proced-narrow
      :n "gr" #'proced)

(map! :map process-menu-mode-map
      :n "gr" #'list-processes)

Projects

Functions

Test jumping / creation

(defun my-project/create-test-file (file-template buffer-file-name->test-file-name)
  (let* ((ext (f-ext (buffer-file-name)))
         (test-file-name (concat (funcall buffer-file-name->test-file-name (f-no-ext (buffer-file-name))) "." ext)))
    (if (f-exists? test-file-name)
        (find-file test-file-name)
      (with-current-buffer (find-file test-file-name)
        (or (-some->> file-template
                      (-id-when #'f-exists?)
                      (f-read)
                      (s-lines)
                      (--drop-while (s-starts-with? "#" it))
                      (s-join "\n")
                      (yas-expand-snippet)
                      (evil-insert-state))
            (message "No default file template found for "))))))

(defun my-project|create-test-file ()
  (interactive)
  (cond
   ((or (eq major-mode 'clojure-mode)
        (eq major-mode 'clojurescript-mode)
        (eq major-mode 'clojurec-mode))
    (my-project/create-test-file (f-join doom-user-dir "file-templates/clojure-mode/__test.clj")
                                 (fn! (concat % "_test"))))
   (t (my-project/create-test-file nil #'identity))))

(defun my-project|jump-to-or-create-test-file ()
  (interactive)
  (condition-case err (projectile-toggle-between-implementation-and-test)
    (error
     (message "No test file found. Creating...")
     (my-project|create-test-file))))

Projectile

Config

Ignored Projects

Dont add packages inside ~~.emacs.d~ to projectile, as I often browse the source for a package, but I dont want them added to my projectile-known-projects.

(use-package! projectile
  :init
  (setq projectile-ignored-projects '("~/"
                                      "/tmp"
                                      "~/.emacs.d/.local/straight/repos/")))
Scan directory for repositories
(use-package! projectile
  :init
  (setq projectile-project-search-path '("~/Code/Repositories"
                                         "~/.config"
                                         "~/Code/Projects"
                                         "~/Code/Smorgasbord/items"
                                         "~/Code/Work/Hyma/"
                                         "~/Code/Work/Hyma/penpot/repo"
                                         "~/Code/Dotfiles"))
  :config
  ;; Auto discover when running switch project for the first time
  (add-transient-hook! 'counsel-projectile-switch-project
    (projectile-cleanup-known-projects)
    (projectile-discover-projects-in-search-path)))

Documents

Pdf

Package

(package! pdf-tools :built-in 'prefer)

Config

Fix midnight colors for doom-one theme
;; Fix midnight colors for doom-one theme
(setq pdf-view-midnight-colors '("#BBC2CD" . "#282C34"))

Bindings

(map!
 :map pdf-occur-buffer-mode-map
 :gn [tab] (cmd! (pdf-occur-goto-occurrence t)))

Ebook

Packages

Major mode for reading EPUBs in Emacs

Package
(package! nov)
Functions
Setup function
(defun my-nov/setup ()
  (setq line-spacing 5)
  (face-remap-add-relative 'variable-pitch :family "Liberation Serif" :height 1.4)
  (setq visual-fill-column-center-text t)
  (setq visual-fill-column-width (+ nov-text-width 25))
  (visual-fill-column-mode t))
Config
(use-package! nov
  :defer t
  :init
  (add-to-list 'auto-mode-alist '("\\.epub\\'" . nov-mode))
  (setq nov-text-width 75)
  (setq nov-save-place-file (f-join doom-local-dir "nov-places"))
  :config
  (add-hook 'nov-mode-hook 'my-nov/setup))

Spell Checking

Packages

Naive linter for english prose.

Config
Disable doom defined hooks for writegood mode
(use-package! writegood-mode
  :hook 'nil)
Disable writegood-mode with spell-fu mode
(defun +spell-fu/activate-writegood-mode-hook ()
  "Toggle writegood mode with spell-fu mode."
  (cond
   (spell-fu-mode (writegood-mode 1))
   (t (writegood-mode -1))))

(add-hook! 'spell-fu-mode-hook :after #'+spell-fu/activate-writegood-mode-hook)

Flyspell

Defer flyspell until first evil-insert-state

Flyspell most of the time is an annoyance when opening a file just for reading. I mostly only need it when writing text (sometimes when reviewing, but then I can toggle it manually).

So I defer it until the first text change has been done (or the first insert state). I’ve tried it on the first-change-hook, but that is also affected by changing todo states for example in org-mode.

(defun +flyspell/load-after-first-insert--activate ()
  "Turns on flyspell after the first buffer change or evil-insert state."
  (remove-hook! '(first-change-hook evil-insert-state-entry-hook) :local #'+flyspell/load-after-first-insert)
  (remove-hook! '(first-change-hook evil-insert-state-entry-hook) :local #'+flyspell/load-after-first-insert--activate)
  (spell-fu-mode 1))

(defun +flyspell/load-after-first-insert ()
  "Turns on flyspell after the first buffer evil-insert state.
Exception for org mode when the header property \"DISABLE_SPELLCHECKER\" is set."
  (cond ((and (equal major-mode 'org-mode) (+org/get-global-property "DISABLE_SPELLCHECKER")) nil)
        (t (add-hook! '(evil-insert-state-entry-hook) :local #'+flyspell/load-after-first-insert--activate))))

(defmacro my-spelling@remove-doom-spell-hooks (package mode)
  `(use-package! ,package
     :init
     (remove-hook! '(org-mode-hook
                     markdown-mode-hook
                     TeX-mode-hook
                     rst-mode-hook
                     mu4e-compose-mode-hook
                     message-mode-hook
                     text-mode-hook
                     git-commit-mode-hook)
       ,mode)))

(my-spelling@remove-doom-spell-hooks spell-fu #'spell-fu-mode)
(my-spelling@remove-doom-spell-hooks flyspell #'flyspell-mode)
(my-spelling@remove-doom-spell-hooks writegood-mode #'writegood-mode)

(add-hook! '(org-mode-hook markdown-mode-hook) :local #'+flyspell/load-after-first-insert)

Functions

Spell correct first word

(defun my-spell-correct|auto-correct (&optional _arg)
  "A bit better spelling auto correct, show recommendations when universal-argument is supplied.
Otherwise take the first recommendation and automatically apply it (which is the correct one most of the time)."
  (interactive "P")
  (save-excursion
    (pcase current-prefix-arg
      (`() (ignore-errors
             (let ((+spell-correct-interface (cl-function (lambda (&rest xs) (first xs)))))
               (call-interactively #'+spell/correct))))
      (`(,_x) (call-interactively #'+spell/correct)))))

Bindings

(use-package! spell-fu
  :general ([remap ispell-word] #'my-spell-correct|auto-correct))

Bindings

Flycheck

Config

Disabled Checkers

Elisp

Disable flycheck completely in org-src-mode, the hints are not useful.

Source: https://github.com/adimit/config/blob/74a7fc0acaf82e18d0ca4bd0ca6e539ff93b13bc/emacs/main.org#checkdoc-in-org-src-edit-buffers

(add-hook 'org-src-mode-hook (lambda () (flycheck-mode -1)))

Completion

Packages

Actions
Search from counsel-find-file
(defun my-counsel/search-from-find-file-counsel (x)
  "Function docstring"
  (let ((default-directory (if (f-dir? x) x (f-dirname x))))
    (+default/search-cwd)))

(after! ivy
 (setq my-counsel::action:search-from-find-file
  '(("s" my-counsel/search-from-find-file-counsel "Search in directory")))
 (ivy-add-actions 'counsel-find-file my-counsel::action:search-from-find-file))
Functions
Project File Jump
(defun my-counsel|project-file-jump (x)
  "Jump to file in project"
  (interactive)
  (counsel-file-jump nil (f-join (projectile-project-root) x)))
External Drives
(defun my-counsel|mounted-drives ()
  "Counsel of mounted drives."
  (interactive)
  (let* ((path (f-join "/run/media/" (user-login-name)))
         (entries (f-entries path)))
    (pcase (length entries)
      (0 (user-error "Error: No mounted devices!"))
      (1 (counsel-find-file (car entries)))
      (_ (counsel-find-file path)))))
Search project for symbol at point

Doom doesn’t double escape | which is needed for rg. Original +default/search-project-for-symbol-at-point

I’ve also removed the feature checking, since I ony use ivy.

(defun my-ivy|search-project-for-symbol-at-point (symbol dir)
  "Search current project for symbol at point.
If prefix ARG is set, prompt for a known project to search from."
  (interactive
   (list (->> (or (doom-thing-at-point-or-region) "")
              (rxt-quote-pcre)
              (s-replace-all '(("|" . "\\|"))))
         (let ((projectile-project-root nil))
           (if current-prefix-arg
               (if-let (projects (projectile-relevant-known-projects))
                   (completing-read "Search project: " projects nil t)
                 (user-error "There are no known projects"))
             (doom-project-root default-directory)))))
  (+ivy/project-search nil symbol dir))

(advice-add #'+default/search-project-for-symbol-at-point :override #'my-ivy|search-project-for-symbol-at-point)
Occur modify for last search

In the occur buffer I quickly want to change the text I searched for. With these functions I can quickly do an evil-ex replacement or a multicursor edit.

(defun my-ivy-occur|evil-ex-last-search ()
  "Evil Ex search for the occur grepped text."
  (interactive)
  (let ((str (ivy-state-text ivy-occur-last)))
    (my-evil/ex-search-str str)
    (evil-ex "%s//")))

(defun my-ivy-occur|evil-mutliedit-last-search ()
  "Evil Ex search for the occur grepped text."
  (interactive)
  (require 'evil-multiedit)
  (ivy-wgrep-change-to-wgrep-mode)
  (let ((str (ivy-state-text ivy-occur-last)))
    (when (search-forward-regexp str)
      (forward-char (- (- (length str) 1)))
      (setq evil-multiedit--dont-recall t)
      (evil-multiedit--start-regexp str (point-min) (point-max)))))
Config
Always show actions in hydra.
(setq ivy-read-action-function #'ivy-hydra-read-action)
Banish mouse cursor

The cursor will steal the focus from ivy-posframe, breaking ivy completely…

This option moves the mouse cursor to 0x0, which is really unacceptable, but works for now.

(setq posframe-mouse-banish t)
Bindings
Minibuffer
(map!
 :after ivy
 :map ivy-minibuffer-map
  "<s-return>" 'ivy-call
  "<C-tab>" 'minibuffer-complete
  "<C-return>" 'ivy-immediate-done
  "M-m" 'ivy-mark
  "M-SPC" 'ivy-restrict-to-matches)
Occur Mode
(map! :map ivy-occur-mode-map
      :gni "RET" #'ivy-occur-press-and-switch)

Better sorting for company

Package
(package! prescient)
(package! company-prescient)
Config
(use-package! prescient
  :config
  (prescient-persist-mode 1))

(use-package! company-prescient
  :after company
  :config
  (company-prescient-mode 1))
Functions
Complete line for all project files
(cl-defun my-grep/search-in-project (str &optional (dir default-directory))
  (let ((default-directory (my-file/project-root dir)))
    (->> (shell-command-to-string (s-join " " (list "rg" "--no-filename"
                                                    (concat "\"" (shell-quote-argument str) "\""))))
         (counsel--split-string)
         (-uniq)
         (-map #'s-trim-left))))

(defun my-company/complete-line-project (command &optional arg &rest ignored)
  "Complete line in all project files."
  (interactive (list 'interactive))
  (require 'company)
  (pcase command
    (`interactive (company-begin-backend 'my-company/complete-line-project))
    (`prefix      (company-grab-line "^[\t\s]*\\(.+\\)" 1))
    (`candidates  (my-grep/search-in-project arg))))
Complete line for all buffers

Complete a whole line with all lines from buffers matching the current major-mode.

(defun my-company/list-buffers-with-mode (modes)
  "Get all buffers that match MODES"
  (--filter
   (with-current-buffer it (-contains? (doom-enlist modes) major-mode))
   (buffer-list)))

(defun my-company/list-buffers-with-major-mode ()
  "Get all buffers matching the current major-mode
Has built in aliases"
  (let ((javascript-modes (list 'rjsx-mode 'js2-mode)))
    (pcase major-mode
      ('rjsx-mode
       (my-company/list-buffers-with-mode javascript-modes))
      ('js2-mode
       (my-company/list-buffers-with-mode javascript-modes))
      (_
       (my-company/list-buffers-with-mode major-mode)))))

(defun my-company/complete-line-all-buffers (command &optional arg &rest ignored)
  "`company-mode' completion backend that completes whole-lines, akin to vim's
C-x C-l."
  (interactive (list 'interactive))
  (require 'company)
  (pcase command
    (`interactive (company-begin-backend 'my-company/complete-line-all-buffers))
    (`prefix      (company-grab-line "^[\t\s]*\\(.+\\)" 1))
    (`candidates
     (all-completions
      arg
      (funcall (-compose
                #'-uniq
                #'-flatten
                (lambda (xs)
                  (--map (with-current-buffer it
                           (split-string
                            (replace-regexp-in-string
                             "^[\t\s]+" ""
                             (buffer-substring-no-properties (point-min) (point-max)))
                            "\\(\r\n\\|[\n\r]\\)" t)) xs)))
               (my-company/list-buffers-with-major-mode))))))

Bindings

(map!
 (:prefix "C-x"
   :i "C-p" #'my-company/complete-line-project
   :i "C-l" #'my-company/complete-line-all-buffers
   :i "C-." #'+company/whole-lines))
Config
Sort company by occurrence
(setq company-transformers '(company-sort-by-occurrence)
      company-idle-delay 0.5)

LSP

Package

(package! lsp-mode :pin "dd61303b2dc989a58f7dddd4c754f828a3f52107")
(package! lsp-ui :pin "072bb29152038518c2478813b82c8d04d07df84c")
(package! lsp-ivy :pin "9ecf4dd9b1207109802bd1882aa621eb1c385106")

Config

Guess root

When working in nested git worktrees, lsp should infer the root by using projectile, by finding the closest .projectile file. This file is always created when creating a worktree.

(setq lsp-auto-guess-root t)

Disable presets

All are these are distracting and not helpful.

  • Removes all popup UIs
  • Remove signature message
  • Remove lsp flycheck
(use-package! lsp
  :config
  (setq lsp-eldoc-render-all nil
        lsp-eldoc-enable-hover nil
        lsp-eldoc-enable-signature-help nil
        lsp-eldoc-prefer-signature-help nil
        lsp-inhibit-message t

        lsp-enable-file-watchers nil

        lsp-eldoc-enable-hover nil
        lsp-signature-auto-activate nil
        lsp-signature-render-documentation nil
        lsp-signature-doc-lines 1

        lsp-diagnostics-provider :none
        lsp-modeline-diagnostics-enable nil
        lsp-modeline-code-actions-enable nil

        lsp-highlight-symbol-at-point nil
        lsp-prefer-flymake nil

        lsp-on-idle-hook nil))

Fix flycheck for js buffers

LSP manually disables all checkers and chooses it’s own. This way you can’t add checkers after your regular hooks like js2-hook. So I fix the checker manually for each mode after lsp was loaded.

(after! lsp-mode
  (remove-hook 'lsp-mode-hook #'+lsp-init-flycheck-or-flymake-h))

(defun +js/fix-checker ()
  "Fix LSP overwritten checkers."
  (interactive)
  (when (-contains? '(js2-mode rjsx-mode) major-mode)
    (flycheck-select-checker 'javascript-eslint)))

(add-hook 'lsp-mode-hook #'+js/fix-checker)

Remove face-highlight

(add-hook! lsp-mode-hook
  (defun my-lsp/add-faces-hook ()
    (set-face-foreground 'lsp-face-highlight-textual nil)
    (set-face-background 'lsp-face-highlight-textual nil)
    (set-face-attribute 'lsp-face-highlight-textual nil :weight 'medium
                        :background (face-background 'macrostep-expansion-highlight-face))))

Functions

Prevent LSP highlighting symbols over multiple lines

Sometimes LSP would highlight stuff accross multiple lines, which is very distracting as it removes all syntax highlighting.

(defun lsp--document-highlight-callback (highlights)
  "Create a callback to process the reply of a
`textDocument/documentHighlight' message for the buffer BUF.
A reference is highlighted only if it is visible in a window."
  (lsp--remove-overlays 'lsp-highlight)

  (let* ((wins-visible-pos (-map (lambda (win)
                                   (cons (1- (line-number-at-pos (window-start win) t))
                                         (1+ (line-number-at-pos (window-end win) t))))
                                 (get-buffer-window-list nil nil 'visible))))
    (setq lsp--have-document-highlights t)
    (-map
     (-lambda ((&DocumentHighlight :range (&Range :start (start &as &Position :line start-line)
                                                  :end (end &as &Position :line end-line))
                                   :kind?))
       (-map
        (-lambda ((start-window . end-window))
          ;; Make the overlay only if the reference is visible
          (let ((start-point (lsp--position-to-point start))
                (end-point (lsp--position-to-point end)))
            (when (and (> (1+ start-line) start-window)
                       (< (1+ end-line) end-window)
                       ;; Only highlight non-multiline strings
                       (let ((str (buffer-substring-no-properties start-point end-point)))
                         (not (s-contains? "\n" str)))
                       (not (and lsp-symbol-highlighting-skip-current
                                 (<= start-point (point) end-point))))
              (-doto (make-overlay start-point end-point)
                (overlay-put 'face (cdr (assq (or kind? 1) lsp--highlight-kind-face)))
                (overlay-put 'lsp-highlight t)))))
        wins-visible-pos))
     highlights)))

Snippets

Overrides

Add the snippet key to to the snippet prompt so I know it for next time. There is no api for it, so I just override the yas--prompt-for-template function.

(after! yasnippet
  (defun yas--prompt-for-template (templates &optional prompt)
    "Interactively choose a template from the list TEMPLATES.

TEMPLATES is a list of `yas--template'.

Optional PROMPT sets the prompt to use."
    (when templates
      (setq templates
            (sort templates #'(lambda (t1 t2)
                                (< (length (yas--template-name t1))
                                   (length (yas--template-name t2))))))
      (cl-some (lambda (fn)
                 (funcall fn (or prompt "Choose a snippet: ")
                          templates
                          (lambda (x) (concat (yas--template-name x) " "
                                              (let ((key (yas--template-key x)))
                                                (when (not (string= key (yas--template-name x)))
                                                  (format "(%s)" (yas--template-key x))))))))
               yas-prompt-functions))))

Functions

Expand snippet by name

(defun my-snippets/insert-by-name (name)
  (require 'noflet)
  (noflet ((dummy-prompt
            (prompt choices &optional display-fn)
            (declare (ignore prompt))
            (or (cl-find name choices :key display-fn :test #'string=)
                (throw 'notfound nil))))
    (let ((yas-prompt-functions '(dummy-prompt)))
      (catch 'notfound
        (yas-insert-snippet t)))))

Use last src language

Used with src snippet to auto fill the language from the previously used src language.

(defun my-snippets/org-src-lang ()
  "Try to find the current language of the src/header at point.
Return nil otherwise."
  (save-excursion
    (pcase
        (downcase
         (buffer-substring-no-properties
          (goto-char (line-beginning-position))
          (or (ignore-errors (1- (search-forward " " (line-end-position))))
              (1+ (point)))))
      ("#+property:"
       (when (re-search-forward "header-args:")
         (buffer-substring-no-properties
          (point)
          (or (and (forward-symbol 1) (point))
              (1+ (point))))))
      ("#+begin_src"
       (buffer-substring-no-properties
        (point)
        (or (and (forward-symbol 1) (point))
            (1+ (point)))))
      ("#+header:"
       (search-forward "#+begin_src")
       (my-snippets/org-src-lang))
      (_ nil))))

(defun my-snippets/org-last-src-lang ()
  (save-excursion
    (beginning-of-line)
    (when (search-backward "#+begin_src" nil t)
      (my-snippets/org-src-lang))))

Config

Disable doom snippets / load custom snippets

(use-package! yasnippet
  :init
  (require 'doom-snippets nil t))

Project Boilerplate

Package

(package! skeletor)

Config

(use-package! skeletor
  :config
  (setq skeletor-project-directory "~/Code/Projects")
  (setq skeletor-user-directory (f-join doom-user-dir "project-templates"))
  (skeletor-define-template "babashka"
    :title "Clojure Babashka"
    :default-license (rx bol "MIT"))
  (skeletor-define-template "clojurescript"
    :title "Clojurescript"
    :default-license (rx bol "MIT"))
  (skeletor-define-template "nix"
    :title "Nix"
    :default-license (rx bol "MIT")))

Search (Grep)

Bindings

Wgrep Mode

(map!
 :map (wgrep-mode-map ivy-occur-grep-mode-map)
 :n [return] #'compile-goto-error

 :localleader
 :desc "Remove line" "d" (cmd! (let ((inhibit-read-only t))
                                 (my-buffer/delete-current-line))))

Web

Packages

No need, screws with entry buffer position and header display…

(package! elfeed-goodies :disable t)

Functions

Open elfeed
(defun my-elfeed|open ()
  (interactive)
  (unless (get-buffer "*elfeed-search*")
    (setq elfeed-search-filter +rss:default-search-filter))
  (elfeed)
  (my-rss|hydra/body))
Filter by unread
(defun my-rss/filter-by-unread ()
  "Show elfeed articles tagged with unread"
  (interactive)
  (elfeed-search-set-filter "@6-months-ago +unread"))
Visit entry DWIM

Open the current entry:

  • With the browser
  • Or if it’s a youtube feed, open with mpv
(defun my-rss|visit-entry-dwim ()
  "Either open the current entry in eww or play it in mpv."
  (interactive)
  (message "")
  (let ((entry (if (eq major-mode 'elfeed-show-mode) elfeed-show-entry (elfeed-search-selected :single)))
        (patterns my-rss:visit-entry-mpv-pattern))
    (while (and patterns (not (string-match (car my-rss:visit-entry-mpv-pattern) (elfeed-entry-link entry))))
      (setq patterns (cdr patterns)))
    (if patterns
        (my-rss:play-entry-with-mpv)
      (if (eq major-mode 'elfeed-search-mode)
          (elfeed-search-browse-url)
        (elfeed-show-visit)))))
Visit entry with MPV
(defvar my-rss:visit-entry-mpv-pattern
  '("youtu\\.?be")
  "List of regexp to match against elfeed entry link to know
whether to use mpv to visit the link.")

(defun my-rss:play-entry-with-mpv ()
  "Play entry link with mpv."
  (interactive)
  (let ((entry (if (eq major-mode 'elfeed-show-mode) elfeed-show-entry (elfeed-search-selected :single)))
        (url (elfeed-entry-link entry)))
    (my-shell/mpv-youtube-url url)))
Visit entry with EWW
(defun my-rss::visit-entry-eww ()
  "Open the current entry with eww."
  (interactive)
  (add-hook 'eww-after-render-hook 'eww-readable nil t)
  (let ((buffer (save-window-excursion
                  (eww (elfeed-entry-link elfeed-show-entry))
                  (current-buffer))))
    (switch-to-buffer buffer)))
Pretty elfeed
(defun my-rss/wrap-entry ()
  "Enhances an elfeed entry's readability by wrapping it to a width of
`fill-column' and centering it with `visual-fill-column-mode'."
  (let ((inhibit-read-only t)
        (inhibit-modification-hooks t))
    (setq-local truncate-lines nil)
    (setq-local shr-width 100)
    (setq-local +zen-text-scale 0.9)
    (mixed-pitch-mode)
    (writeroom-mode)
    (set-buffer-modified-p nil)))

(add-hook 'elfeed-show-mode-hook #'my-rss/wrap-entry)
Update feeds when saving org-elfeed file

When I save the elfeed org-mode file I want to automatically update the feed list. Update function taken from elfeed package.

⚠ Disabled for now as it leaks to other modes

(defun my-rss/update-feeds-on-save ()
  (when (and (eq major-mode 'org-mode)
             (string= buffer-file-name (car rmh-elfeed-org-files))))
  (require 'elfeed)
  (rmh-elfeed-org-process rmh-elfeed-org-files rmh-elfeed-org-tree-id))

(add-hook! 'after-save-hook #'my-rss/update-feeds-on-save)
Capture entry
(defun my-rss::capture/stringify-entry (entry)
  (let ((title (->> (elfeed-entry-title entry)
                    ;; Remove braces, they're just confusing to org links
                    (s-replace "[" "")
                    (s-replace "]" "")))
        (url (elfeed-entry-link entry))
        (author (->> (elfeed-meta entry :authors)
                     car
                     ((lambda (x) (plist-get x :name)))))
        (tags (-some--> (elfeed-entry-tags entry)
                (-remove-item 'unread it)
                (-map #'symbol-name it)
                (s-join ":" it)
                (s-wrap it ":" ":"))))
    (template "[[<<url>>][<<author>> - <<title>>]] <<(when tags tags)>>")))

(defun my-rss::capture|capture ()
  "Capture current entry of elfeed."
  (interactive)
  (let* ((entry (if (eq major-mode 'elfeed-show-mode)
                   elfeed-show-entry
                 (elfeed-search-selected :single)))
         (org-entry (my-rss::capture/stringify-entry entry)))
       (kill-new org-entry)
       (if (s-contains? org-entry "youtube.com")
           (org-capture nil "ew")
        (org-capture nil "er"))))
Hydra
(defun my-rss/toggle-filter (tag)
  "Toggle elfeed filter TAG or append to filters if non-existent."
  (->> (or (-when-let* (((_ modi) (s-match (t! "\\_<\\([+-]\\)<<tag>>\\_>") elfeed-search-filter))
                        (new-modi (if (string= modi "+") "-" "+")))
             (s-replace-regexp (t! "\\_<\\<<modi>><<tag>>\\_>") (t! "<<new-modi>><<tag>>") elfeed-search-filter))
           (s-append (t! " +<<tag>>") elfeed-search-filter))
       (--tap (progn (setq elfeed-search-filter it)
                     (elfeed-search-update--force)
                     (-log (t! "Filter changed to: <<it>>"))))))

(defun my-rss|counsel-toggle-filter (&optional arg)
  "Toggles a filter in elfeed."
  (interactive "P")
  (ivy-read (t! "Filter: <<elfeed-search-filter>>") (elfeed-db-get-all-tags)
            :action #'my-rss/toggle-filter
            :caller #'my-rss|counsel-toggle-filter))

(defhydra my-rss|hydra ()
   "Elfeed"
   ("j" (evil-next-line) "Next item" :column "Navigate")
   ("k" (evil-previous-line) "Previous item")
   ("t" (my-rss|counsel-toggle-filter) "Tags" :column "Filter")
   ("0" (elfeed-search-set-filter "@2-weeksago +unread") "Unread" :column "Favorites")
   ("f" (elfeed-search-set-filter "@2-weeksago +unread -YOUTUBE -REDDIT") "Feeds")
   ("y" (elfeed-search-set-filter "@2-weeksago +unread +YOUTUBE") "Youtube")
   ("e" (elfeed-search-set-filter "@2-weeksago +unread +REDDIT =emacs") "Reddit: Emacs")
   ("n" (elfeed-search-set-filter "@2-weeksago +unread +REDDIT =nixos") "Reddit: Nixos")
   ("U" (elfeed-update) "Update" :column "actions")
   ("u" (elfeed-search-untag-all-unread) "Mark as unread")
   ("s" (elfeed-db-save) "Save DB")
   ("v" (my-rss|visit-entry-dwim) "Visit")
   ("q" nil "Quit" :color blue))

Config

elfeed-org file
(setq rmh-elfeed-org-files (list (+org/expand-org-file-name "Elfeed/Elfeed.org")))
Disable sliced images

These don’t work if you have a big line-height.

(setq +rss-enable-sliced-images nil)
Default search
(setq +rss:default-search-filter "@2-week-ago +unread -YOUTUBE")
(setq-default elfeed-search-filter +rss:default-search-filter)

Bindings

Search Mode Bindings
(map!
 :after elfeed
 :map elfeed-search-mode-map
 :gn "r" #'elfeed-update
 (:prefix-map ("g" . "Go")
  :desc "Youtube" :gn "y" (cmd! (elfeed-search-set-filter "@2-week-ago +YOUTUBE +unread"))
  :desc "Normal"  :gn "n" (cmd! (elfeed-search-set-filter "@2-week-ago -YOUTUBE +unread"))
  :desc "Month"   :gn "m" (cmd! (elfeed-search-set-filter "@1-month-ago +unread"))))
Show Mode Bindings
(map!
 :after elfeed
 :map elfeed-search-mode-map
 :gn "r" #'elfeed-update)
LocalLeader
(map! :map (elfeed-search-mode-map elfeed-show-mode-map)
      :localleader
      :desc "Filter Hydra" "f" #'my-rss|hydra/body
      :desc "Capture"      "c" #'my-rss::capture|capture
      :desc "Open Eww"     "o" #'my-rss::visit-entry-eww
      :desc "Visit"        "v" #'my-rss|visit-entry-dwim
      (:prefix-map ("s" . "Search")
       :desc "Unread" "u" #'my-rss/filter-by-unread))

IRC

Config

(after! circe
  (set-irc-server! "chat.freenode.net"
    `(:tls t
      :port 6697
      :nick "floscr"
      :sasl-username ,(+pass-get-user "Irc/freenode.net")
      :sasl-password (lambda (&rest _) (+pass-get-secret "Irc/freenode.net"))
      :channels ("#emacs" "#nixos"))))

Config

Pretty buffer setup
(defun my-eww*pretty-buffer-setup ()
  "Function docstring"
  (setq-local truncate-lines t)
  (setq-local shr-width 120)
  (setq fill-column 120)
  (setq-local visual-fill-column-center-text t)
  (visual-line-mode 1)
  (visual-fill-column-mode 1))

(add-hook! 'eww-mode-hook #'my-eww*pretty-buffer-setup)
Set the max page width
(setq shr-width 100)

Calendar

Functions

Filtered Calendars

Personal (Without Work)
(defun my-calendar::filtered|personal (&rest args)
  (interactive)
  (let ((org-agenda-skip-function '(+org/agenda-skip-without-match "-WORK-HIDE_CALENDAR")))
      (call-interactively #'=calendar)))
Personal (Just Family)
(defun my-calendar::filtered|just-family (&rest args)
  (interactive)
  (let ((org-agenda-skip-function '(+org/agenda-skip-without-match "+FAMILY|+BIRTHDAY")))
      (call-interactively #'=calendar)))
Personal (Just Birthdays)
(defun my-calendar::filtered|birthdays (&rest args)
  (interactive)
  (let ((org-agenda-files `(,(+org/expand-org-file-name "Main/contacts.org")))
        (org-agenda-skip-function '(+org/agenda-skip-without-match "+BIRTHDAY")))
      (call-interactively #'=calendar)))
Personal (Without Family)
(defun my-calendar::filtered|no-family (&rest args)
  (interactive)
  (let ((org-agenda-skip-function '(+org/agenda-skip-without-match "-FAMILY-WORK-HIDE_CALENDAR")))
      (call-interactively #'=calendar)))
Work
(defun my-calendar::filtered|work (&rest args)
  (interactive)
  (let ((org-agenda-skip-function '(+org/agenda-skip-without-match "+WORK-HIDE_CALENDAR")))
      (call-interactively #'=calendar)))

Config

Custom Holidays (Austrian)

Emacs comes with a lot of custom calendars that I don’t want. This is mostly copied from How to set up a german emacs calendar.

(use-package! calfw
  :config
  (setq my-calendar::holidays:general
        '((holiday-fixed 1 1 "New Years")
          (holiday-fixed 5 1 "1st Mai")))
  (setq my-calendar::holidays:austria
        `((holiday-fixed 1 6 "Heilige drei Könige")
          (holiday-fixed 5 1 "Staatsfeiertag")
          (holiday-fixed 8 15 "Mariä Himmelfahrt")
          (holiday-fixed 10 26 "Nationalfeiertag")
          (holiday-fixed 11 1 "Allerheiligen")
          (holiday-fixed 12 8 "Mariä Empfängnis")
          (holiday-fixed 12 24 "Weihnachten")
          (holiday-fixed 12 25 "Christtag")
          (holiday-fixed 12 26 "Stefanitag")
          (holiday-easter-etc -2 "Karfreitag")
          (holiday-easter-etc 0 "Ostersonntag")
          (holiday-easter-etc 1 "Ostermontag")
          (holiday-easter-etc 39 "Christi Himmelfahrt")
          (holiday-easter-etc 49 "Pfingstsonntag")
          (holiday-easter-etc 50 "Pfingstmontag")
          (holiday-easter-etc 60 "Fronleichnam")))
  (setq calendar-holidays (append
                           my-calendar::holidays:general
                           my-calendar::holidays:austria
                           holiday-solar-holidays)))
General Holidays
(after! calfw
  :config
  (setq general-holidays
        '((holiday-fixed 1 1 "New Years")
          (holiday-fixed 5 1 "1st Mai"))))
Austrian Holidays
(after! calfw
  :config
  (setq austrian-holidays
        `((holiday-fixed 1 6 "Heilige drei Könige")
          (holiday-fixed 5 1 "Staatsfeiertag")
          (holiday-fixed 8 15 "Mariä Himmelfahrt")
          (holiday-fixed 10 26 "Nationalfeiertag")
          (holiday-fixed 11 1 "Allerheiligen")
          (holiday-fixed 12 8 "Mariä Empfängnis")
          (holiday-fixed 12 24 "Weihnachten")
          (holiday-fixed 12 25 "Christtag")
          (holiday-fixed 12 26 "Stefanitag")
          ;; variable
          (holiday-easter-etc -2 "Karfreitag")
          (holiday-easter-etc 0 "Ostersonntag")
          (holiday-easter-etc 1 "Ostermontag")
          (holiday-easter-etc 39 "Christi Himmelfahrt")
          (holiday-easter-etc 49 "Pfingstsonntag")
          (holiday-easter-etc 50 "Pfingstmontag")
          (holiday-easter-etc 60 "Fronleichnam"))))
Set calendars
(after! calfw
  :config
  (setq calendar-holidays
        (append
          general-holidays
          austrian-holidays
          holiday-solar-holidays)))

General

(after! calfw
  :config
  (setq calendar-week-start-day 1)
  (setq calendar-time-display-form
        '(24-hours ":" minutes (and time-zone (concat " (" time-zone ")"))))
  (setq calendar-abbrev-length 2))

Calc

Config

Additional Units

(setq math-additional-units '((GB "1024 * MiB" "Giga Byte")
                              (MB "1024 * KiB" "Mega Byte")
                              (KB "1024 * B" "Kilo Byte")
                              (B nil "Byte")))

Terminal

Bindings

Allow evil enter anywhere

Fix for comint mode, throwing an error when pressing enter in the middle of the line.

error in process filter: End of buffer
(map!
 :after comint
 :map comint-mode-map
 :ni "RET" (cmd! (comint-send-input nil t))
 :n "<C-backspace>" #'comint-clear-buffer)

Functions

Groot
(defun my-eshell-cmd|groot ()
  (interactive)
  (setq default-directory (projectile-project-root)))
Syntax highlighted cat
(defun my-eshell/colorized-cat (file)
  "Like `cat' but output with Emacs syntax highlighting."
  (with-temp-buffer
    (insert-file-contents file)
    (let ((buffer-file-name file))
      (delay-mode-hooks
        (set-auto-mode)
        (if (fboundp 'font-lock-ensure)
            (font-lock-ensure)
          (with-no-warnings
            (font-lock-fontify-buffer)))))
    (buffer-string)))

Config

Custom Aliases
(setq my-eshell:aliases
  '(("q"      "exit")
    ("cat"    "my-eshell/colorized-cat $1")
    ("f"      "find-file $1")
    ("bd"     "eshell-up $1")
    ("rg"     "rg --color=always $*")
    ("ag"     "ag --color=always $*")
    ("l"      "ls -lh")
    ("ll"     "ls -lah")
    ("gs"     "git status")
    ("groot"  "(my-eshell-cmd|groot)")
    ("gc"     "git commit")
    ("grha"   "git reset --hard; git clean -f -d")
    ("clear"  "clear-scrollback")))

(mapcar (lambda (alias)
          (set-eshell-alias! (car alias) (cadr alias)))
        my-eshell:aliases)

package

Package
(package! eat)
Config
(use-package! eat
  :commands (eat)
  :init
  (setq! eat-kill-buffer-on-exit t
         eat-term-terminfo-directory (expand-file-name "~/.emacs.d/.local/straight/repos/eat/terminfo")
         eat-enable-yank-to-terminal t)
  :config
  (defun +eat-deleted-window-after-kill-buffer ()
    (if (featurep 'evil) (evil-window-delete) (delete-window)))
  (defun +eat-setup ()
    (add-hook! 'kill-buffer-hook :local '+eat-deleted-window-after-kill-buffer))
  (add-hook! 'eat-mode-hook '+eat-setup)
  (pushnew! evil-emacs-state-modes 'eat-mode))

Org Mode

Custom Packages

Indirect Narrow Buffers

Open org tasks in an indirect popup that can be closed via C-c C-c. The indirect buffer is narrowed to the current item. This makes visiting org tasks much more focused, without having to widen the main buffer every time.

Known Issues:

  • this popup blocks the org-todo popup
  • The C-c C-c binding overrides the clocking binding
Config
(set-popup-rule! "^\\*Org Indirect" :side 'bottom :size 0.35 :quit t :ttl nil :select t :autosave)
Minor Mode
(defvar my-org-indirect:window-mode-map (make-sparse-keymap))

(define-minor-mode my-org-indirect-window-mode
  "Open org headlines in an indirect window buffer."
  :keymap my-org-indirect:window-mode-map)
Bindings
(map! :map my-org-indirect:window-mode-map
      "C-c C-c" #'my-org-indirect|save-and-kill-window
      "C-c C-k" #'kill-buffer-and-window
      :localleader
      :desc "Show original" "+" #'my-org-indirect|show-original)
Advices
Close window after refiling
(defadvice! +org/org-refile-close-indirect-window (&rest _)
  "Close indirect buffer windows after refiling them."
  :after '(org-refile)
  (when (my-org-indirect-window-mode)
    (kill-buffer-and-window)))
Functions
Main
(cl-defun my-org-indirect|narrow-subtree-indirect (&key visit-fn goto-parent? (popup? t))
  "Narrow to an indirect buffer in a popup."
  (interactive)
  (-let* (((src-buffer heading-text)
           (save-window-excursion
             (save-excursion
               (progn
                 (when visit-fn (funcall visit-fn))
                 ;; save-window-excursion cancels org-mode being fully loaded
                 ;; this leads to the indirect buffer not knowing where the source comes frome
                 ;; and the org cycle methods not working
                 (org-mode)
                 (list
                  (current-buffer)
                  (+org/org-heading-text))))))
          (dst-buffer-name (template "*Org Indirect <<(buffer-name src-buffer)>> <<heading-text>>*"))
          (existing-buffer (--find (s-starts-with? dst-buffer-name (buffer-name it)) (buffer-list))))
    (cond
     (existing-buffer
      (if popup?
          (pop-to-buffer (buffer-name existing-buffer))
        (switch-to-buffer buffer)))
     (t
      ;; Cleanup old indirect buffers
      (kill-matching-buffers "^\\*Org Indirect.*" nil t)
      (with-current-buffer src-buffer
        (let ((buffer (clone-indirect-buffer dst-buffer-name nil)))
          (with-current-buffer buffer
            (widen)
            (if popup?
                (pop-to-buffer buffer)
              (switch-to-buffer buffer))
            (setq header-line-format "Edit, then exit with 'C-c C-c', abort with 'C-c C-k'.")
            (save-excursion
              (when goto-parent?
                (+org|org-topmost-todo-element)
                ;; Show CHILDREN without content
                (org-global-cycle 4))
              (org-narrow-to-subtree))
            ;; When the item has subtrees show only the subtrees
            (ignore-errors
              (if (save-excursion (search-forward-regexp "^\\*" nil t))
                  (org-global-cycle 10)
                ;; Otherwise show contents but not the drawers
                (org-cycle 4)))
            (my-org-indirect-window-mode 1)
            (rename-buffer (concat
                            (buffer-name)
                            (int-to-string (point-min))
                            (int-to-string (point-max))
                            "*"))
            (evil-forward-word-begin 1)
            buffer)))))))
Window Killing
(defun my-org-indirect|save-and-kill-window ()
  "Save the buffer and close the indirect buffer and window."
  (interactive)
  (save-buffer)
  (kill-buffer-and-window))

(defun my-org-indirect/kill-buffer-maybe ()
  "Kill the window/buffer if it's indirect.
For example when archiving a task, there would be an empty window left over."
  (when (s-match "^\\*Org Indirect" (buffer-name))
    (kill-buffer-and-window)))
Show original
(defun my-org-indirect|show-original ()
  "Show the original buffer of the indirect window."
  (interactive)
  (doom/widen-indirectly-narrowed-buffer)
  (call-interactively #'+popup/raise)
  (my-org-indirect-window-mode -1)
  (setq header-line-format nil)
  (call-interactively #'evil-scroll-line-to-center))
Visiting Functions
(defun my-org-indirect|clock-visit-entry (&optional arg)
  "Visit currently clocked org entry in a narrowed indirect buffer."
  (interactive "P")
  (let ((org-agenda-follow-indirect t))
    (my-org-indirect|narrow-subtree-indirect :visit-fn #'org-clock-goto
                                             :goto-parent? (not arg))))

(defun my-org-indirect|agenda-visit-entry (arg)
  "Visit agenda entry in a narrowed indirect buffer."
  (interactive "P")
  (my-org-indirect|narrow-subtree-indirect :visit-fn #'org-agenda-switch-to
                                           :goto-parent? (not arg)))
Inserting New Items

I want to create items directly from the org agenda. For this I will visit the current item, move to the header and add a new child at the bottom.

(defun my-org-indirect|add-entry ()
  (interactive)
  (with-current-buffer (my-org-indirect|narrow-subtree-indirect :visit-fn #'org-agenda-switch-to
                                                                :goto-parent? t)
    (widen)
    (call-interactively #'+org/insert-item-below)
    (org-narrow-to-element)))

Packages

ob-async enables asynchronous execution of org-babel src blocks via the :async property.

Config
(use-package! ob-async
  :after org-babel)
Configuration
Directories
(setq org-roam-directory (+org/expand-org-file-name "Roam"))
(setq org-roam-db-location (+org/expand-org-file-name "org-roam.db"))

org-protocol

(use-package! org-protocol)
Package
(package! ob-nix :recipe (:host codeberg :repo "theesm/ob-nix") :pin "76d71b37fb")
Config
(use-package! ob-nix)

org-babel

Functions
Eval clojure org blocks with babashka

Executes clojure with Babashka and clojurescript with babashka/nbb. nbb needs to be installed via npm

npm i -g nbb
(with-eval-after-load 'ob-clojure
  (defcustom org-babel-clojure-backend nil
    "Backend used to evaluate Clojure code blocks."
    :group 'org-babel
    :type '(choice
            (const :tag "inf-clojure" inf-clojure)
            (const :tag "cider" cider)
            (const :tag "slime" slime)
            (const :tag "bb" bb)
            (const :tag "Not configured yet" nil)))

  (defun elisp->clj (in)
    (cond
     ((listp in) (concat "[" (s-join " " (mapcar #'elisp->clj in)) "]"))
     (t (format "%s" in))))

  (defun ob-clojure-eval-with-bb (expanded params)
    "Evaluate EXPANDED code block with PARAMS using babashka."
    (unless (executable-find "bb")
      (user-error "Babashka not installed"))
    (let* ((target (a-get params :target))
           (clojurescript? (string= target 'cljs))
           (stdin (let ((stdin (cdr (assq :stdin params))))
                    (when stdin
                      (elisp->clj
                       (org-babel-ref-resolve stdin)))))
           (input (cdr (assq :input params)))
           ;; Wrap clojurescript in print statement since nbb doesn't print the last result (like node)
           (expanded (if clojurescript?
                         (format "(print %s)" expanded)
                       expanded))
           (file (make-temp-file "ob-clojure-bb" nil nil expanded))
           (executable (or (cond
                            (clojurescript? (executable-find "nbb"))
                            (t "bb --prn"))
                           (user-error "Could not find executable for target %s!" target)))
           (command (concat (when stdin (format "echo %s | " (shell-quote-argument stdin)))
                            ;; nbb has different input params...
                            (if clojurescript?
                              (format "%s %s"
                                      executable
                                      (shell-quote-argument file))
                              (format "%s %s -f %s"
                                      executable
                                      (cond
                                       ((equal input "edn") "")
                                       ((equal input "text") "-i")
                                       (t ""))
                                      (shell-quote-argument file)))))
           (result (shell-command-to-string command)))
      (s-trim result)))

  (defun org-babel-execute:clojure (body params)
    "Execute a block of Clojure code with Babel."
    (unless org-babel-clojure-backend
      (user-error "You need to customize org-babel-clojure-backend"))
    (let* ((expanded (org-babel-expand-body:clojure body params))
           (result-params (cdr (assq :result-params params)))
           result)
      (setq result
            (cond
             ((eq org-babel-clojure-backend 'inf-clojure)
              (ob-clojure-eval-with-inf-clojure expanded params))
             ((eq org-babel-clojure-backend 'cider)
              (ob-clojure-eval-with-cider expanded params))
             ((eq org-babel-clojure-backend 'slime)
              (ob-clojure-eval-with-slime expanded params))
             ((eq org-babel-clojure-backend 'bb)
              (ob-clojure-eval-with-bb expanded params))))
      (org-babel-result-cond result-params
        result
        (condition-case nil (org-babel-script-escape result)
          (error result)))))

  (customize-set-variable 'org-babel-clojure-backend 'bb))

(add-hook 'org-mode-hook (lambda () (require 'ob-clojure)))
Org Babel Snippets
(defun my-org-babel/get-body ()
  "Get the body of an org babel block."
  (save-excursion
    (let ((element (org-element-at-point)))
      (when (eq (org-element-type element) 'src-block)
        (org-element-property :value element)))))

(defun my-org-babel/expand-in-popup-buffer ()
  (interactive)
  (let* ((src-block-body (my-org-babel/get-body))
         (popup-buffer (generate-new-buffer "*Org Babel Yas*")))
    (with-current-buffer popup-buffer
      (text-mode)
      (yas-expand-snippet src-block-body))
    (pop-to-buffer popup-buffer)))

Export org as github flavored markdown

  • ``` Code blocks with syntax descriptor
Package
(package! ox-gfm)
Functions

org-src-mode

Bindings
Go to source buffer instead of dired
(defun my-org-src-mode/goto-source-marker-buffer (&optional other-window file-name)
  "Go to the source file for a org-src-mode buffer instead of dired."
  (interactive)
  (org-goto-marker-or-bmk org-src--beg-marker))

(map!
 :map org-src-mode-map
 :m "-"  #'my-org-src-mode/goto-source-marker-buffer)

Functions

Link at point

Use with destructuring like this

(-let (((&plist* title) (my-org/link-at-point)))
  title)
Should match these kind of links
Source
(defun my-org/link-at-point ()
  "Returns a plist '(el url title) with title as nilable.
When no link is found return nil."
  (let ((link-info (org-element-context)))
    (when (eq (org-element-type link-info) 'link)
      (let* ((type (org-element-property :type link-info))
             (url (concat type ":" (org-element-property :path link-info)))
             (title (org-element-property :title link-info))
             (region (list (org-element-property :begin link-info)
                           (org-element-property :end link-info))))
        (when url
          (list :el link-info
                :url url
                :title title
                :region region))))))

Jump to definition for tangled files

Take me to the literate source file when using find-function etc.

(defadvice! +org|try-org-babel-tangle-jump-to-org (&optional arg1)
  :after '(find-function
           find-variable)
  (ignore-errors
    (org-babel-tangle-jump-to-org)))

Get url title

Fetches the link title for the current link using the get_url_title command from dotfiles/get_url_title.clj at 07156f2d059da2676d13d17d67eb8fed596557db · floscr/dotfiles · GitHub

(defun my-org/fetch-link-title-command (url)
  "Fetches the link title with get_url_title (https://github.com/floscr/nim-utils#get_url_title)."
  (or (my/shell-command-str "get_url_title" url "--org")
      (user-error "Error: Could not get title for url \"%s\"" url)))

(defun my-org/fetch-link-title (&optional fn)
  "Fetch the link title under the cursor."
  (-when-let* ((org-el (my-org/link-at-point)))
    (-let* ((fn (or fn #'my-org/fetch-link-title-command))
            ((&plist :el :url :region) org-el)
            (title (funcall fn url)))
      (when (string= title "Could not find title in html")
        (user-error (t! "Error: Could not fetch title for url <<url>>")))
      (list :region region
            :title title))))

(defun my-org/update-link-title (&optional fn)
  "Replace the org link under the cursor with the link with the fetched title."
  (-when-let* (((&plist :region :title) (my-org/fetch-link-title fn)))
    (apply #'delete-region region)
    (insert title)))

(defun my-org|fetch-this-org-link-rss ()
  "Fetches the title for the plain link under the cursor."
  (interactive)
  (let ((fn (lambda (url)
              (my/shell-command-str "get_url_title" "rss" url))))
    (my-org/update-link-title fn)))

(defun my-org|fetch-this-org-link-title ()
  "Fetches the title for the plain link under the cursor."
  (interactive)
  (my-org/update-link-title))

(defun my-org-agenda|fetch-this-org-link-title ()
  "Fetches the title for the plain link under the cursor."
  (interactive)
  (require 'org-ml)
  (save-window-excursion
    (org-agenda-switch-to)
    (-when-let* ((link-pos (-some->> (org-ml-parse-this-headline)
                             (org-ml-get-property :title)
                             (--find (org-ml-is-type 'link it))
                             (org-ml-get-property :begin))))
      (goto-char link-pos)
      (my-org|fetch-this-org-link-title)))
  (org-agenda-redo))

Read timestamp to string

Use the interactive timestamp interface to read an org string

(defun my-org/read-timestamp-inactive-str ()
  (with-temp-buffer
    (org-time-stamp-inactive)
    (buffer-substring-no-properties (point-min) (point-max))))

Copy link under Cursor

;;;###autoload
(defun my-org/match-link (pos)
  "Return cons of link under point and its position.
POS to match:
0: the entire org link,
1: the url,
2: the title."
  (let ((pos (org-in-regexp org-link-bracket-re 1))
        (str (org-link-unescape (match-string-no-properties pos))))
    (cons str pos)))

;;;###autoload
(defun my-org/match-org-link-entire ()
  "Match the link under the cursor as (str . pos)"
  (my-org/match-link 0))

;;;###autoload
(defun my-org/match-org-link-url ()
  "Match the link url under the cursor as (str . pos)"
  (my-org/match-link 1))

;;;###autoload
(defun my-org/match-org-link-title ()
  "Match the link title under the cursor as (str . pos)"
  (my-org/match-link 2))

(defun my-org/copy-or-cut-link (match-fn &optional cut?)
  "Copy or cut the current link under the cursor"
  (my/kill-and-message (car (funcall match-fn)))
  (when cut?
    (-when-let ((link . (beg . end)) (my-org/match-org-link-entire))
      (delete-region beg end))))

(defun my-org|copy-entire-link (&optional cut?)
  (interactive "P")
  (my-org/copy-or-cut-link #'my-org/match-org-link-entire cut?))

(defun my-org|copy-link-url (&optional cut?)
  (interactive "P")
  (my-org/copy-or-cut-link #'my-org/match-org-link-url cut?))

(defun my-org|copy-link-title (&optional cut?)
  (interactive "P")
  (my-org/copy-or-cut-link #'my-org/match-org-link-title cut?))

Copy As Markdown

(defun my-org/copy-as-markdown (&optional subtree-p)
  "Copy the current subtree as markdown to clipboard."
  (require 'ox-gfm)
  (let* ((org-export-with-toc nil)
         (org-export-with-special-strings nil)
         (org-export-with-smart-quotes nil)
         (md-file (make-temp-file "my-md-export"))
         (md (org-export-to-file 'gfm md-file nil subtree-p))
         (content (f-read md-file)))
    (kill-new content)
    (message "Copied buffer as markdown to clipboard.")))

(defun my-org|copy-buffer-as-markdown ()
  "Copy the entire buffer as markdown to clipboard."
  (interactive)
  (my-org/copy-as-markdown))

(defun my-org|copy-subtree-as-markdown ()
  "Copy the subtree as markdown to clipboard."
  (interactive)
  (my-org/copy-as-markdown t))

Add created date to headings

(defun my-org|add-created-property ()
  "Add CREATED property with the current time to the current item."
  (interactive)
  (org-set-property +org-created-property (+org/inactive-timestamp)))

(defun my-org/add-created-property-automatically ()
  "Add CREATED property with the current time to the current item."
  (interactive)
  (let ((global-property (+org-get-global-property "ADD_CREATED")))
    (unless (string= global-property "nil")
      (when (+org-get-global-property "ADD_CREATED")
        (org-set-property +org-created-property (+org/inactive-timestamp))))))
(advice-add '+org/insert-item-below :after (my@ignore-args #'my-org/add-created-property-automatically))
(advice-add '+org/insert-item-above :after (my@ignore-args #'my-org/add-created-property-automatically))

Add id to headings

  • When file has global property #+ADD_ID: t
  • Is a file in org-directory
(defun my-org/add-id-property-automatically ()
  "Add CREATED property with the current time to the current item."
  (interactive "P")
  (when (or (string= (+org-get-global-property "ADD_ID") "t")
            (f-ancestor-of? org-directory buffer-file-name))
    (call-interactively #'org-id-get-create)))
(advice-add '+org/insert-item-below :after (my@ignore-args #'my-org/add-id-property-automatically))
(advice-add '+org/insert-item-above :after (my@ignore-args #'my-org/add-id-property-automatically))

Interactively get current org heading level

(defun my-org/match-heading (s)
  (s-match (rx (group (+ "*")) whitespace (+ any)) s))

(defun my-org/heading-level ()
  (save-excursion
    (org-up-element)
    (or (-some-> (substring-no-properties (thing-at-point 'line))
                 (s-trim)
                 (my-org/match-heading)
                 (second)
                 (length))
        0)))

(defun my-org/next-heading-level ()
  (+ (my-org/heading-level) 1))

(defun my-org/next-heading-stars ()
  (-> (my-org/next-heading-level)
      (s-repeat "*")))
Test
(assert (equal (my-org/match-heading "*** foo") '("*** foo" "***")))
(assert (equal (my-org/heading-level) 5))
(assert (equal (my-org/next-heading-level) 6))
(assert (equal (my-org/next-heading-stars) "******"))

Find unused attachments

(defun my-dired|search-attach-at-point ()
  (interactive)
  (+ivy/project-search nil (f-base (dired-file-name-at-point)) (f-expand org-directory)))

Meta DWIM

Config

Org Src Language Aliases

(after! org-src
  (add-to-list 'org-src-lang-modes '("clj" . clojure))
  (add-to-list 'org-src-lang-modes '("cljs" . clojurescript))
  (add-to-list 'org-src-lang-modes '("tsx" . typescript))
  (add-to-list 'org-src-lang-modes '("ts" . typescript))
  (add-to-list 'org-src-lang-modes '("jsx" . js-jsx)))

Custom attachments

I truly dislike how org-attach handles attachments.

The attachments get sorted by an id of the files directory and their headline. This is truly against the mantra of raw text files, as you have to move the file if you want to move the link.

This made me loose countless files because I just moved the link text to another headline!

My solution is to have an extra link that resolves to ONE directory with all the files in it. I will make sure I don’t override any files (easily enough…)

(defvar my-attach:dir "~/Documents/Org/.attach/clojure-attach")

(comment
 (let* ((buffer (get-buffer-create "temp"))
        (_ (-> (my-org/link-at-point)
               (print buffer)))
        (str (with-current-buffer buffer
               (buffer-substring-no-properties (point-min) (point-max)))))
   (s-match ":path \"\\(.+?\\)\"" str))
 nil)

(defun my-attach/resolve-path (path)
  (let* ((attach-dir (expand-file-name my-attach:dir))
         (path* (expand-file-name path attach-dir)))
    path*))

(defun my-attach/org-url->path (url)
  (-> (s-replace-regexp "^my-attach:" "" url)
      (my-attach/resolve-path)))

(defun my-attach/path-at-point ()
  (-some-> (my-org/link-at-point)
           (plist-get :url)
           (my-attach/org-url->path)))

(defun my-attach|delete-attachment ()
  "Deletes attachment under cursor and removes the link."
  (interactive)
  (-when-let* (((&plist :url :region) (my-org/link-at-point)))
    (-let (((beg end) region)
           (path (my-attach/org-url->path url)))
      (f-delete path)
      (delete-region beg end))))

(defun my-attach/follow (path)
  (let ((path* (my-attach/resolve-path path)))
    (if (or (string-match-p (image-file-name-regexp) path)
            (f-ext-p path "pdf"))
        (find-file path*)
      (my-dired::open-external/xdg-open path*))))

(defun my-attach/image-data-fun (protocol path _for)
  (let ((attach-dir (expand-file-name my-attach:dir)))
    (when (and (display-graphic-p)
               (string-match-p (image-file-name-regexp) path))
      (let ((file (expand-file-name path attach-dir)))
        (when (and (file-exists-p file)
                   (image-type-from-file-name file))
          (with-temp-buffer
            (set-buffer-multibyte nil)
            (setq buffer-file-coding-system 'binary)
            (insert-file-contents-literally file)
            (buffer-substring-no-properties (point-min) (point-max))))))))

(org-link-set-parameters "my-attach"
                         :follow #'my-attach/follow
                         :display #'org-link
                         :image-data-fun #'my-attach/image-data-fun)

(defun my-attach|dwim ()
  (interactive)
  (let* ((ext (condition-case nil
                  (with-timeout (0.2 'aborted)
                    (image-type-from-data (x-get-clipboard)))
                (error 'aborted)))
         (file-name (cond
                     ((eq ext 'aborted) (my-file/last-modified-file-in-dir "~/Media/Screencapture"))
                     (ext (let* ((file-name (make-temp-file nil nil (concat "." (symbol-name ext)))))
                            (f-write (x-get-clipboard) 'binary file-name)
                            file-name))
                     ((my/clipboard-binary?) nil)
                     (t (x-get-clipboard)))))
    (when file-name
      (save-evil-excursion
       (when (evil-normal-state-p)
         (evil-append 0))
       (-some-> (my/shell-command-str "org_attach" file-name "--attach-dir" (f-expand my-attach:dir))
                (s-trim)
                (insert))))))

(defun my-attach|last-download ()
  (interactive)
  (save-evil-excursion
   (when (evil-normal-state-p)
     (evil-append 0))
   (when-let ((file-name (my-file/last-modified-file-in-dir "~/Downloads")))
     (-some->> (my/shell-command-str "org_attach" file-name "--attach-dir" (f-expand my-attach:dir))
               (s-trim)
               (insert)))))

Use external Image Converter

(setq image-use-external-converter t)

Meta Ret DWIM

(defun my-org|meta-ret-dwim (&optional arg)
  (interactive "P")
  (let* ((attach (my-attach/path-at-point)))
    (cond
     (attach (my-shell/async-command-no-window (concat "feh " attach)))
     (t (call-interactively #'org-meta-return arg)))))

Bindings

(map!
 :map org-mode-map
 :desc "Org Meta Return Dwim" :g "M-RET" #'my-org|meta-ret-dwim)

Hydras

Work

Hyma / Penpot

Config

Functions

Apply DEV env
(defun my-hyma|apply-custom-dev-script ()
  "Applies custom dev-script and assumes it unchanged in git."
  (interactive)
  (let ((files '("backend/scripts/start-dev"
                 "backend/src/app/config.clj"
                 "docker/devenv/docker-compose.yaml")))
    (dolist (file files)
      (magit-no-assume-unchanged file))
    (-when-let ((rev msg) (->> (my-magit/stash-list)
                               (-find (fn! (-let (((rev msg) %))
                                             (string= msg "DEV"))))))
      (magit-stash-apply rev))
    (dolist (file files)
      (magit-assume-unchanged file))))

Pitch

Create worktree

(defun my-pitch|create-worktree ()
  "Creates a worktree for a branch.
Applies the custom dev script and copies over node_modules + bundle caches to reduce network/computation activity."
  (interactive)
  (when-let* ((path (call-interactively #'my-magit|create-worktree-project-from-branch)))
    ;; (my-pitch|apply-custom-dev-script)
    (let ((src (my-file/git-root))
          (dst (my-file/project-root)))
      (call-process-shell-command (t! "rsync -ravE <<src>>/node_modules <<dst>>&") nil 0 nil)
      (call-process-shell-command (t! "rsync -ravE <<src>>/desktop-app/node_modules <<dst>>/desktop-app &") nil 0 nil))))
(after! magit
  (transient-append-suffix 'magit-worktree "b" '("p" "Pitch worktree" my-pitch|create-worktree)))

Start terminal

(defun my-pitch|start-dev-terminal ()
  (interactive)
  (let ((default-directory (my-file/project-root nil)))
    ;; (my-pitch|apply-custom-dev-script)
    (call-process-shell-command "alacritty -e $SHELL -c './scripts/pit dev --desktop'&" nil 0 nil)))

Convert to uix

(defun my-pitch/convert-hiccup-to-uix ()
  (interactive)
  (save-excursion
    (clojure-convert-collection-to-list)
    (forward-char 1)
    (insert "$ ")))

Convert to gridSize

(defun my-pitch|insert-grid-size-from-px ()
  "Convert px to gridSize macro and insert result."
  (interactive)
  (let* ((size (read-number "Size: "))
         (base-size 12.0)
         (new-size (/ size base-size))
         (can-floor (not (/= new-size (floor new-size))))
         (new-size (if can-floor (floor new-size) new-size)))
    (insert (t! "gridSize(<<new-size>>)"))))

Fix Clojure

(defun my-pitch|autofix-clojure-buffer ()
  "Autofix the "
  (interactive)
  (let ((buffer (current-buffer)))
    (deferred:$
      (deferred:process-shell (s-join " " (list "clojure" "-M:nsorg-fix" "-M:cljfmt-fix" "<" buffer-file-name)))
      (deferred:nextc it
        (with-current-buffer buffer (revert-buffer))
        (message "Autofixed buffer!")))))

Debug

(defun my-pitch|debug-statement ()
  (interactive)
  (let* ((clojure-symbol (pcase (doom-thing-at-point-or-region)
                           ("" (save-excursion
                                 (lispyville-backward-sexp-begin 1)
                                 (doom-thing-at-point-or-region)))
                           (t (doom-thing-at-point-or-region))))
         (placeholder (if clojure-symbol (concat "dd-" (substring-no-properties clojure-symbol)) "dd"))
         (name (read-string "Debug var name: " placeholder))
         (suffix (-some->> (s-split-up-to "-" name 1)
                   (-second-item)
                   ((lambda (x) (s-wrap x "\"")))))
         (snippet (s-join "\n"
                          (list
                           ""
                           (t! "(defn <<name>> [x]")
                           (if suffix
                               (t! "  (js/console.log <<suffix>> x)")
                             (t! "  (js/console.log x)"))
                           (t! "  (set! js/global-debug-<<name>> x) x)")
                           ""
                           (t! "(comment js/global-debug-<<name>>)")
                           ""))))
    (save-excursion
      (lispyville-wrap-round 1)
      (insert (t! "<<name>> "))
      (lispyville-backward-function-begin)
      (previous-line 1)
      (insert snippet))))

(defun my-pitch|eval-debug-statement ()
  (interactive)
  (save-excursion
    (goto-char (point-min))
    (search-forward "(comment js/global-debug" nil nil nil)
    (lispyville-up-list)
    (goto-char (- (point) 1))
    (cider-eval-last-sexp)))

Custom builder

(defun my-pitch|apply-custom-dev-script ()
  "Applies custom dev-script and assumes it unchanged in git."
  (interactive)
  (let* ((script-path "desktop-app/scripts/dev-all.py")
         (content (->> (f-expand "~/Code/Work/Pitch/dev-overwrite/dev-all.py")
                       f-read))
         (dst-path (f-join (my-file/git-root nil) script-path)))
    (f-write content 'utf-8 dst-path)
    (magit-assume-unchanged script-path)))

Goto l10n

(defun my-pitch|l10n-jump (identifier &optional arg)
  (interactive
   (list (doom-thing-at-point-or-region)
         current-prefix-arg))
  (let ((l10n-dir (f-join (projectile-project-root) "projects/l10n/src"))
        (id (->> identifier
                 substring-no-properties
                 (s-replace-regexp "^:" ""))))
    (+ivy-file-search :query id :in l10n-dir)))

Convert string to l10n

(defun my-clojure/string-content-at-point ()
  "Returns a list with the string content at point without quotes and the begin and end positions of the string with quotes."
  (if (clojure--in-string-p)
    (save-excursion
      (let ((beg (progn (goto-char (clojure-string-start))
                        (forward-char 1)
                        (point)))
            (end (progn
                   (search-forward-regexp "\"" (point-at-eol) nil)
                   (backward-char 1)
                   (point))))
        (list (buffer-substring-no-properties beg end) (cons (- beg 1) (+ 1 end)))))
    (user-error "Cursor not positioned on a clojure string.")))

(defvar my-pitch:l10n-key nil
  "A prefix to always use for l10n string conversion in a buffer.")
(make-local-variable 'my-pitch:l10n-key)

(defun my-pitch|set-l10n-key-from-region ()
  "Sets the l10n buffer prefix from the region."
  (interactive)
  (setq my-pitch:l10n-key (buffer-substring-no-properties (region-beginning) (region-end))))

(defun my-pitch|convert-string-to-l10n ()
  "Convert the string under the cursor to l10n"
  (interactive)
  (-let* (((original-string (original-beg . original-end)) (my-clojure/string-content-at-point))
          (clojure-key (concat ":" (s-dashed-words original-string))))
    (delete-region original-beg original-end)
    (insert clojure-key)
    (lispy-wrap-round 1)
    (insert "l10n/t")
    (search-forward ":")
    (-let* (((beg . end) (bounds-of-thing-at-point 'symbol))
            (l10n-key (->> (buffer-substring-no-properties beg end)
                           (s-replace ":" "")
                           (s-prepend my-pitch:l10n-key)
                           (read-string "l10n key: "))))
      (delete-region beg end)
      (insert ":" l10n-key)
      (kill-new (concat l10n-key " = " original-string)))))

Create pull request

(defun my-pitch|ghub-create-pull-request ()
  "Function docstring"
  (interactive)
  (shell-command "gh pr create --web"))

Create pull request md

(defun my-pitch|create-pull-request-md ()
  "Create a markdown document for the currently open branch."
  (interactive)
  (let* ((branch-name (magit-get-current-branch))
         (branch-name (if (magit-name-remote-branch branch-name)
                          (->> branch-name
                               (magit-name-remote-branch)
                               (magit-split-branch-name)
                               (cdr)
                               (s-replace "florian/" ""))
                        (concat  (format-time-string "%y-%m") "-" branch-name)))
         (file (f-join "/home/floscr/Code/Work/Pitch/pr-docs" (concat branch-name ".org")))
         ;; Prevent file template being applied for file
         (+file-templates-alist nil))
    (find-file-other-window file)
    (when (eq (buffer-size) 0)
      ;; (my-snippets/insert-by-name "Pull Request Template")
      (goto-char (point-min)))))

Format buffer

(defun my-pitch|format-buffer ()
  "Formats the current buffer."
  (interactive)
  (let* ((bin (f-join (my-file/project-root) "./scripts/pit"))
         (cmd (cond
               ((or (eq major-mode 'clojure-mode)
                    (eq major-mode 'clojurescript-mode))
                (concat bin " fmt2 " (shell-quote-argument buffer-file-name)))
               ((eq major-mode 'rjsx-mode)
                (concat "prettier -w " (shell-quote-argument buffer-file-name))))))
    (shell-command cmd nil nil)
    (revert-buffer)))

Stylus Variables

(defun my-pitch/get-css-variables ()
  (let* ((path (f-join (projectile-project-root) "projects/frontend/src/app/pitch/styles/helpers.styl"))
         (content (f-read path)))
    (->> (s-lines content)
         (--filter (s-starts-with? "$" it)))))

(defun my-pitch/get-css-variables-value (line)
  (->> line
       (s-split " ")
       (car)))

Paste stylus fixed

(defun my-pitch|paste-stylus-fixed ()
  (interactive)
  (let* ((k (current-kill 0))
         (default-directory "/home/floscr/Code/Work/Pitch/pitch-app/.worktrees/figma-to-stylus/projects/figma-to-stylus")
         (result (shell-command-to-string (concat "./node_modules/.bin/nbb ./src/figma_to_stylus/core.cljs" (combine-and-quote-strings (s-split "\n" k))))))
    (insert result)))

Pairing mode

(defun my-pitch*activate-lsp-header ()
  (when lsp-mode
    (setq-local lsp-headerline-breadcrumb-segments '(symbols))
    (lsp-headerline-breadcrumb-mode 1)
    (setq lsp-headerline-arrow "")
    (set-face-attribute 'lsp-headerline-breadcrumb-separator-face nil :underline 'unspecified)
    (set-face-attribute 'lsp-headerline-breadcrumb-path-face nil :underline 'unspecified)
    (set-face-attribute 'lsp-headerline-breadcrumb-path-error-face nil :underline 'unspecified)
    (set-face-attribute 'lsp-headerline-breadcrumb-path-warning-face nil :underline 'unspecified)
    (set-face-attribute 'lsp-headerline-breadcrumb-path-info-face nil :underline 'unspecified)
    (set-face-attribute 'lsp-headerline-breadcrumb-path-hint-face nil :underline 'unspecified)
    (set-face-attribute 'lsp-headerline-breadcrumb-project-prefix-face nil :underline 'unspecified)
    (set-face-attribute 'lsp-headerline-breadcrumb-unknown-project-prefix-face nil :underline 'unspecified)
    (set-face-attribute 'lsp-headerline-breadcrumb-symbols-face nil :underline 'unspecified)
    (set-face-attribute 'lsp-headerline-breadcrumb-symbols-error-face nil :underline 'unspecified)
    (set-face-attribute 'lsp-headerline-breadcrumb-symbols-warning-face nil :underline 'unspecified)
    (set-face-attribute 'lsp-headerline-breadcrumb-symbols-info-face nil :underline 'unspecified)
    (set-face-attribute 'lsp-headerline-breadcrumb-symbols-hint-face nil :underline 'unspecified)
    (set-face-attribute 'lsp-headerline-breadcrumb-deprecated-face nil :underline 'unspecified)))

(defvar my-pitch:pairing-mode nil)

(defun my-pitch|start-pairing ()
  (interactive)
  (shell-command "systemctl --user stop picom.service")
  (doom-big-font-mode 1)
  (add-hook! 'window-configuration-change-hook #'my-pitch*activate-lsp-header)
  (my-pitch*activate-lsp-header)
  (lsp-lens-mode -1)
  (setq my-pitch:pairing-mode t)
  (set-face-attribute 'line-number nil
                      :foreground (face-attribute 'font-lock-doc-face :foreground)))

(defun my-pitch|stop-pairing ()
  (interactive)
  (doom-big-font-mode 0)
  (remove-hook! 'window-configuration-change-hook #'my-pitch*activate-lsp-header)
  (lsp-headerline-breadcrumb-mode -1)
  (lsp-lens-mode 1)
  (setq my-pitch:pairing-mode nil)
  (doom/reload-theme))

(defun my-pitch|toggle-pairing ()
  (interactive)
  (if my-pitch:pairing-mode
      (my-pitch|stop-pairing)
    (my-pitch|start-pairing)))

AI

package

Package
(package! chatgpt-shell :recipe (:host github :repo "xenodium/chatgpt-shell"))
Config
(use-package! chatgpt-shell
  :config
  (setq chatgpt-shell-openai-key
      (lambda ()
        (-> (f-read "/run/agenix/openai")
            (s-trim)))))

Security

Fix GPG saving issue in Emacs 29

Keeping Secrets in Emacs with GnuPG and Auth Sources - Mastering Emacs

(fset 'epg-wait-for-status 'ignore)

Accounting

Beancount

Config

(use-package! beancount
  :config
  (setq beancount-number-alignment-column 80)
  (after! apheleia-core
    (setq apheleia-formatters (delq (assoc 'bean-format apheleia-formatters) apheleia-formatters))))

Documents

Attach document

(defun my-documents|attach ()
  (interactive)
  (let ((f (or (-some-> (dired-get-marked-files) first)
               (buffer-file-name)))
        (default-directory "/home/floscr/.config/dotfiles/new/modules/scripts"))
    (prn (s-join "" (list "bb ./src/bdocs.clj" f)))
    (shell-command-to-string (s-join " " (list "bb ./src/bdocs.clj" (concat "'" f "'"))))))

Media

listen.el

Requires emacs 29

Package
(package! listen :recipe (:host github :repo "alphapapa/listen.el"))
Config
(use-package listen
  :ensure t
  :defer t
  :commands (listen))

Projects

Penpot

Config

(def-project-mode! my-penpot-project-mode
  :modes '(clojure-mode clojurescript-mode magit-mode)
  :when (s-ends-with? "Code/Work/Hyma/tokens-studio-for-penpot/" (doom-project-root))
  :on-enter (progn
              (setq-local my-comment-header:col-char nil)
              (setq-local my-comment-header:col-count 0)
              (let* ((h ";; ===")
                     (comment-modes (a-assoc my-comment-header:mode-comment-start
                                             'my-penpot-project-mode h
                                             'clojurescript-mode h
                                             'clojurec-mode h
                                             'clojure-mode h)))
                (setq-local my-comment-header:mode-comment-start comment-modes))))

Org Web

Config

(def-project-mode! my-org-web-mode
  :modes '(clojure-mode clojurescript-mode magit-mode)
  :when (s-starts-with? "/home/floscr/Code/Smorgasbord/items/org-web" (doom-project-root))
  :on-enter
  (progn
    (defvar emmet-clojure-snippets
      (ht
       ("db" ":display \"block\"")
       ("dg" ":display \"grid\"")
       ("dn" ":display \"none\"")

       ("poi" ":pointer-events \"|\"")
       ("u" ":user-select \"|\"")

       ("bg" ":background \"|\"")
       ("bgc" ":background-color \"|\"")

       ("df" ":display \"flex\"")
       ("fg" ":flex-grow \"|\"")
       ("fs" ":flex-shrink \"|\"")
       ("g" ":gap \"|\"")
       ("fdc" ":flex-direction \"column\"")
       ("jcc" ":justify-content \"center\"")
       ("aic" ":align-items \"center\"")

       ("w" ":width \"|\"")
       ("h" ":height \"|\"")
       ("as" ":aspect-ratio \"1/1\"")

       ("posa" ":position \"absolute\"")
       ("posr" ":position \"relative\"")
       ("posf" ":position \"fixed\"")
       ("b" ":bottom \"|\"")
       ("r" ":right \"|\"")
       ("l" ":left \"|\"")
       ("t" ":top \"|\"")
       ("i" ":inset 0")
       ("zi" ":z-index \"|\"")

       ("p" ":padding \"|\"")
       ("pt" ":padding-top \"|\"")
       ("pr" ":padding-right \"|\"")
       ("pl" ":padding-left \"|\"")
       ("pb" ":padding-bottom \"|\"")

       ("brd" ":border-radius \"|\"")

       ("ov" ":overflow \"hidden\"")
       ("os" ":overflow \"scroll\"")))

    (setq-local emmet-css-major-modes '(clojurescript-mode))
    (setq-local emmet-css-snippets emmet-clojure-snippets)
    (emmet-mode 1)))

Functions

Make css class

(defun org-web/css-import? ()
  (save-excursion
    (goto-char (point-min))
    (re-search-forward "app.utils.css.core :refer-macros" nil t)))

(defun org-web/insert-css-require ()
  (goto-char (point-min))
  (re-search-forward "(:require" nil t)
  (insert "\n" "   [app.utils.css.core :refer-macros [css]"))

(defun org-web/ensure-css-import ()
  (save-excursion
    (when (not (org-web/css-import?))
      (org-web/insert-css-require))))

(defun org-web|make-class ()
  (interactive)
  (when-let ((var-name (symbol-at-point)))
    (or (my-comment-header/goto-headers "Styles")
        (progn
          (goto-char (point-min))
          (evil-jump-item)
          (forward-line)
          (my-comment-header|insert "Styles")))
    (end-of-line)
    (forward-line)
    (when (my-comment-header|goto-next-comment-header)
      (previous-line))
    (insert "\n")
    (insert (t! "(css <<var-name>> [] \n  {})\n"))
    (backward-char 3)
    (evil-insert-state))
  (org-web/ensure-css-import))

Bindings

(map! :map my-org-web-mode-map
      :localleader
      (:prefix ("m" . "modify")
       :desc "Make css class" "c" #'org-web|make-class))

Bindings

General

(map! :g "C-±" #'+popup/raise)

Disable emacs-state-toggle

(map! :nm "C-z" nil)

Auto indent bracket openings

Automatically indent and format when opening area betwen to delimiters.

(defun my-text/indent-between-pair (&rest _ignored)
  (newline)
  (indent-according-to-mode)
  (forward-line -1)
  (indent-according-to-mode))

(sp-local-pair 'prog-mode "{" nil :post-handlers '((my-text/indent-between-pair "RET")))
(sp-local-pair 'prog-mode "[" nil :post-handlers '((my-text/indent-between-pair "RET")))
(sp-local-pair 'prog-mode "(" nil :post-handlers '((my-text/indent-between-pair "RET")))

Modifier

(map! :g "M-RET" #'lsp-find-references)

Super

(map!
 (:map override
   :g "s-g" (cmd! (gptel-send 1))
   :g "s-n" #'evil-buffer-new
   :g "s-;" #'eval-expression
   :g "s-a" #'mark-whole-buffer
   :g "s-s" #'save-buffer
   :g "s-v" #'yank
   :g "s-x" #'execute-extended-command
   :g "s-y" #'helm-show-kill-ring

   ;; Text scale
   :g "s-="   #'doom/increase-font-size
   :g "s--"   #'doom/decrease-font-size
   :g "s-0"   #'doom/reset-font-size))

Evil

Config

Use global ex by default

I almost always want global search and replace, doom changed this in 1a6f50864. To undo this behavior just add g flag

(setq evil-ex-substitute-global t)

Motions

Additional text objects

  • q for any type of quote
  • B for curly braces
  • r for square brackets
(after! evil
  (require 'evil-textobj-anyblock)
  (evil-define-text-object my-evil-textobj-anyblock-inner-quote
    (count &optional beg end type)
    "Select the closest outer quote."
    (let ((evil-textobj-anyblock-blocks
           '(("'" . "'")
             ("\"" . "\"")
             ("`" . "`")
             ("" . ""))))
      (evil-textobj-anyblock--make-textobj beg end type count nil)))

  (evil-define-text-object my-evil-textobj-anyblock-a-quote
    (count &optional beg end type)
    "Select the closest outer quote."
    (let ((evil-textobj-anyblock-blocks
           '(("'" . "'")
             ("\"" . "\"")
             ("`" . "`")
             ("" . ""))))
      (evil-textobj-anyblock--make-textobj beg end type count t)))

  (define-key evil-inner-text-objects-map "q" 'my-evil-textobj-anyblock-inner-quote)
  (define-key evil-outer-text-objects-map "q" 'my-evil-textobj-anyblock-a-quote)
  (define-key evil-inner-text-objects-map "r" 'evil-inner-bracket)
  (define-key evil-inner-text-objects-map "B" 'evil-inner-curly))

Up to next/previous indent

Text objects for lines after or before the current line, that have the same or deeper indent.

(defun evil-indent-plus--line-down-indent-range (&optional point)
  (require 'evil-indent-plus)
  (let* ((range (evil-indent-plus--same-indent-range point))
         (base (point))
         (begin (point)))
    (list begin (cl-second range) base)))

(evil-define-text-object evil-indent-plus-i-indent-line-down (&optional count beg end type)
  "Text object describing the block with the same (or greater) indentation as the current line,
and the line above, skipping empty lines."
  :type line
  (require 'evil-indent-plus)
  (evil-indent-plus--linify (evil-indent-plus--line-down-indent-range)))

(define-key evil-inner-text-objects-map "+" 'evil-indent-plus-i-indent-line-down)

(defun evil-indent-plus--line-up-indent-range (&optional point)
  (require 'evil-indent-plus)
  (let* ((range (evil-indent-plus--same-indent-range point))
         (base (point))
         (begin (point)))
    (list begin (cl-first range) base)))

(evil-define-text-object evil-indent-plus-i-indent-line-up (&optional count beg end type)
  "Text object describing the block with the same (or greater) indentation as the current line,
and the line above, skipping empty lines."
  :type line
  (require 'evil-indent-plus)
  (evil-indent-plus--linify (evil-indent-plus--line-up-indent-range)))

(define-key evil-inner-text-objects-map "-" 'evil-indent-plus-i-indent-line-up)

Little Word Motion

(defun load-evil-camel-case-motion ()
  (require 'evil-little-word)
  (define-key evil-normal-state-map (kbd "M-w") 'evil-forward-little-word-begin)
  (define-key evil-normal-state-map (kbd "M-b") 'evil-backward-little-word-begin)
  (define-key evil-operator-state-map (kbd "M-w") 'evil-forward-little-word-begin)
  (define-key evil-operator-state-map (kbd "M-b") 'evil-backward-little-word-begin)
  (define-key evil-visual-state-map (kbd "M-w") 'evil-forward-little-word-begin)
  (define-key evil-visual-state-map (kbd "M-b") 'evil-backward-little-word-begin)
  (define-key evil-visual-state-map (kbd "i M-w") 'evil-inner-little-word))

(load-evil-camel-case-motion)

Sort Motion

(evil-define-operator +evil/sort (beg end)
  "Sort lines with motion"
  (interactive "<r>")
  (sort-lines nil beg end))

(map!
 (:after evil
    :m "gS" #'+evil/sort))

Case Conversion

Copied code from strickinato/evil-briefcase since it’s not maintained anymore. Convert case motion via Z.

Example pressing ZciW would convert the inner Word into camelCase

(evil-define-operator +evil/case-upper (beg end type)
  "Convert text to upper case."
  (if (eq type 'block)
      (evil-apply-on-block #'s-upcase beg end nil)
    (let ((str (s-upcase (buffer-substring-no-properties beg end))))
      (delete-region beg end)
      (insert str))))

(evil-define-operator +evil/case-lower (beg end type)
  "Convert text to lowercase."
  (if (eq type 'block)
      (evil-apply-on-block #'s-downcase beg end nil)
    (let ((str (s-downcase (buffer-substring-no-properties beg end))))
      (delete-region beg end)
      (insert str))))

(evil-define-operator +evil/case-camel-upper (beg end type)
  "Convert text to CamelCase with a Capital C"
  (if (eq type 'block)
      (evil-apply-on-block #'s-upper-camel-case beg end nil)
    (let ((str (s-upper-camel-case (buffer-substring-no-properties beg end))))
      (delete-region beg end)
      (insert str))))

(evil-define-operator +evil/case-camel-lower (beg end type)
  "Convert text to camelCase with a small C"
  (if (eq type 'block)
      (evil-apply-on-block #'s-lower-camel-case beg end nil)
    (let ((str (s-lower-camel-case (buffer-substring-no-properties beg end))))
      (delete-region beg end)
      (insert str))))

(evil-define-operator +evil/case-snake-lower (beg end type)
  "Convert text to snake_case, slithering"
  (if (eq type 'block)
      (evil-apply-on-block #'s-snake-case beg end nil)
    (let ((str (s-snake-case (buffer-substring-no-properties beg end))))
      (delete-region beg end)
      (insert str))))

(evil-define-operator +evil/case-snake-upper (beg end type)
  "Convert text to SNAKE_CASE, AKA SCREAMING_SNAKE_CASE"
  (if (eq type 'block)
      (evil-apply-on-block #'s-snake-case beg end nil)
    (let ((str (s-upcase (s-snake-case (buffer-substring-no-properties beg end)))))
      (delete-region beg end)
      (insert str))))

(evil-define-operator +evil/case-kebab-upper (beg end type)
  "Convert text to KEBAB-KASE, mmmm... THICK MEAT"
  (if (eq type 'block)
      (evil-apply-on-block #'s-dashed-words beg end nil)
    (let ((str (s-dashed-words (buffer-substring-no-properties beg end))))
      (delete-region beg end)
      (insert (upcase str)))))

(evil-define-operator +evil/case-kebab-lower (beg end type)
  "Convert text to kebab-kase, mmmm... hyphens"
  (if (eq type 'block)
      (evil-apply-on-block #'s-dashed-words beg end nil)
    (let ((str (s-dashed-words (buffer-substring-no-properties beg end))))
        (delete-region beg end)
        (insert str))))

(map!
 (:after evil
  :m "Zu" #'+evil/case-upper
  :m "Zl" #'+evil/case-lower
  :m "ZC" #'+evil/case-camel-upper
  :m "Zc" #'+evil/case-camel-lower
  :m "ZS" #'+evil/case-snake-upper
  :m "Zs" #'+evil/case-snake-lower
  :m "ZK" #'+evil/case-kebab-upper
  :m "Zk" #'+evil/case-kebab-lower))

Org Src Block

Matches any block with BEGIN_, e.g.: SRC blocks.

(defun +evil-org/src-block ()
  "Matches src blocks using org-element-context."
  (-when-let* ((ctx (org-element-context))
               (type (car ctx))
               (ctx (cond
                     ((equal type 'src-block) ctx)
                     ((equal type 'example-block) ctx)
                     ((equal type 'quote-block) ctx)
                     ;; Inside quote blocks org-element-context matches the paragraphs
                     ;; So we have to take the parent block to get the quote-block
                     ((equal type 'paragraph)
                      (-some->> ctx
                        (nth 1)
                        ((lambda (x) (doom-plist-get x :parent)))
                        (--id-when (equal (car it) 'quote-block)))))))
    ctx))

(evil-define-text-object +evil-org-inner-src-block (count &optional beg end type)
  "Select an org src/quote/example block object."
  (evil-org-select-inner-element (+evil-org/src-block)))

(evil-define-text-object +evil-org-an-src-block (count beg end type)
  "An org object.
Matches urls and table cells."
  (evil-org-select-an-element (+evil-org/src-block)))

(after! evil-org
  (evil-define-key '(visual operator) evil-org-mode-map
    "ib" #'+evil-org-inner-src-block
    "ab" #'+evil-org-an-src-block))

Normal Bindings

Window navigation

Fix window navigation for various modes. I don’t like pressing C-w or SPC w as leader to navigate, so I have to work around it:

(map!
 :en "C-h"   #'evil-window-left
 :en "C-j"   #'evil-window-down
 :en "C-k"   #'evil-window-up
 :en "C-l"   #'evil-window-right)

(map!
 :map (image-mode-map
       magit-diff-mode-map
       magit-revision-mode-map
       magit-status-mode-map
       eshell-mode-map
       evil-org-mode-map)
 :en "C-h"   #'evil-window-left
 :en "C-j"   #'evil-window-down
 :en "C-k"   #'evil-window-up
 :en "C-l"   #'evil-window-right)

(add-hook! 'eshell-first-time-mode-hook
  (map!
   :map eshell-mode-map
   :en "C-h"   #'evil-window-left
   :en "C-j"   #'evil-window-down
   :en "C-k"   #'evil-window-up
   :en "C-l"   #'evil-window-right))

(map!
 :map org-agenda-mode-map
 "C-h"  #'evil-window-left
 "C-j"  #'evil-window-down
 "C-k"  #'evil-window-up
 "C-l"  #'evil-window-right)

History navigation in minibuffer

(define-key minibuffer-local-map "\C-p" 'previous-history-element)
(define-key minibuffer-local-map "\C-n" 'next-history-element)

Jump to last buffer

(map! :n "gb" #'evil-switch-to-windows-last-buffer)

Select last paste

(defun +evil|select-pasted ()
  (interactive)
  (let ((start-marker (evil-get-marker ?\[))
        (end-marker (evil-get-marker ?\])))
    (evil-visual-select start-marker end-marker)))

(map! :n "gp" #'+evil|select-pasted)

Jump source dwim

(map! :m "-" #'my-buffer|jump-source-dwim)

Visual Line Mode Navigation

(map! :map visual-line-mode-map
      :nv "j" #'evil-next-visual-line
      :nv "k" #'evil-previous-visual-line)

Insert Bindings

Insert Mode bindings, mostly unicode insertion and workaround for german umlaut.

Insert from the kill ring in insert mode

(map! :i "A-y" #'helm-show-kill-ring)

Unicode Characters

(map!
 :i "C-y"   #'helm-show-kill-ring
 :i "M-`"   (cmd! (insert "°"))
 :i "M-."   (cmd! (insert ""))
 :i "M-^"   (cmd! (insert "°"))
 :i "M-l"   (cmd! (insert "λ"))
 :i "M-w"   (cmd! (insert ""))
 :i "M-i"   (cmd! (insert "")))

Square Bracket Bindings

Global [ & ] combinator bindings

Dumb Jump

(map!
 :n "]F" #'dumb-jump-go
 :n "[F" #'dumb-jump-back)

Flycheck Error Jumping

(map!
 :n "]e" #'flycheck-next-error
 :n "[e" #'flycheck-previous-error)

Projectile Alternate file finding

(map!
 :n "]1" #'my-project|jump-to-or-create-test-file
 :n "[1" #'my-project|jump-to-or-create-test-file)

Comman Bindings

(map!
 :map (clojurescript-mode-map clojure-mode-map)
 :n ",T" #'clojure-thread)

(map!
 :map (clojurescript-mode-map clojure-mode-map emacs-lisp-mode-map)
 :n ",d" #'sp-kill-sexp
 :n ",," #'sp-forward-sexp
 :n ",y" #'my-sp/yank-sexp
 :n ",u" #'sp-backward-up-sexp)

Text Objects

Custom evil text objects mostly stolen from Spacemacs|define-text-object-regexp.

Utils

Define Text Objects
(defmacro +evil/define-text-object-regexp (key name start-regexp end-regexp)
  "Define a text object.
START-REGEXP and END-REGEXP are the boundaries of the text object."
  (let ((inner-name (make-symbol (concat "evil-inner-" name)))
        (outer-name (make-symbol (concat "evil-outer-" name))))
    `(progn
       (evil-define-text-object ,inner-name (count &optional beg end type)
         (evil-select-paren ,start-regexp ,end-regexp beg end type count nil))
       (evil-define-text-object ,outer-name (count &optional beg end type)
         (evil-select-paren ,start-regexp ,end-regexp beg end type count t))
       (define-key evil-inner-text-objects-map ,key (quote ,inner-name))
       (define-key evil-outer-text-objects-map ,key (quote ,outer-name)))))

Config

(+evil/define-text-object-regexp "~" "tilde" "~" "~")
(+evil/define-text-object-regexp "=" "equal" "=" "=")
(+evil/define-text-object-regexp "|" "bar" "|" "|")
(+evil/define-text-object-regexp "*" "star" "*" "*")
(+evil/define-text-object-regexp "$" "dollar" "$" "$")
(+evil/define-text-object-regexp "%" "percent" "%" "%")
(+evil/define-text-object-regexp "/" "slash" "/" "/")
(+evil/define-text-object-regexp "_" "underscore" "_" "_")
(+evil/define-text-object-regexp "-" "hyphen" "-" "-")

Quotes Text Object

Changes the text matching inside quotes with q motion (e.g. ciq) Change inner bracket with r

(after! evil
  (require 'evil-textobj-anyblock)
  (evil-define-text-object my-evil-textobj-anyblock-inner-quote
    (count &optional beg end type)
    "Select the closest outer quote."
    (let ((evil-textobj-anyblock-blocks
           '(("'" . "'")
             ("\"" . "\"")
             ("`" . "'")
             ("" . ""))))
      (evil-textobj-anyblock--make-textobj beg end type count nil)))

  (evil-define-text-object my-evil-textobj-anyblock-a-quote
    (count &optional beg end type)
    "Select the closest outer quote."
    (let ((evil-textobj-anyblock-blocks
           '(("'" . "'")
             ("\"" . "\"")
             ("`" . "'")
             ("" . ""))))
      (evil-textobj-anyblock--make-textobj beg end type count t)))

  (define-key evil-inner-text-objects-map "q" 'my-evil-textobj-anyblock-inner-quote)
  (define-key evil-outer-text-objects-map "q" 'my-evil-textobj-anyblock-a-quote)
  (define-key evil-inner-text-objects-map "r" 'evil-inner-bracket))

Ex Commands

Extend the default evil ex commands from +commands.el

(after! evil-ex
  :config
  (evil-ex-define-cmd "W" #'evil-write))

MultiEdit

(map! :nv "C-M-d" #'evil-multiedit-match-all)

Jump with tab

(map! :n [tab] (general-predicate-dispatch nil
                 (and (featurep! :editor fold)
                      (save-excursion (end-of-line) (invisible-p (point))))
                 #'+fold/toggle
                 (fboundp 'evil-jump-item)
                 #'evil-jump-item)
      :v [tab] (general-predicate-dispatch nil
                 (and (bound-and-true-p yas-minor-mode)
                      (or (eq evil-visual-selection 'line)
                          (not (memq (char-after) (list ?\( ?\[ ?\{ ?\} ?\] ?\))))))
                 #'yas-insert-snippet
                 (fboundp 'evil-jump-item)
                 #'evil-jump-item))

Folding

(define-key! 'global
  [remap evil-toggle-fold]   #'my-comment-header|toggle-fold-or-fallback)

Paste Fix

Evil pastes at the current cursor, which I don’t expect it to do. Most of the time it requires me to move into insert mode, move one character, and then do my function.

(defun +evil/normal-mode-paste-fix (fn &optional insert-char)
  "Move forward one character and then paste."
  (interactive)
  (if (and (evil-normal-state-p)
           (my-buffer/line-contains "[^\s]"))
      (progn
        (forward-char 1)
        (when (and insert-char (looking-at insert-char))
          (insert insert-char))
        (let ((line (substring-no-properties (thing-at-point 'line))))
          (call-interactively fn)
          (when (eq line (substring-no-properties (thing-at-point 'line)))
            (delete-char 1))))
    (call-interactively fn)))

Store all yanks in extra register

(defvar +evil:last-yank nil)

(defun +evil/save-to-yank-register (&rest _rest)
  "Save the last yank to a specific register that only gets overwritten by yanks.
So deletes wont affect this register."
  (setq +evil:last-yank (evil-get-register ?\")))

(defun +evil/insert-from-yank-register ()
  "Save the last yank to a specific register that only gets overwritten by yanks.
So deletes wont affect this register."
  (-some->> +evil:last-yank
    (insert)))

;; (advice-add 'evil-yank :after #'+evil/save-to-yank-register)

Leader

(map!
 :leader
 "'"  #'+popup/toggle
 "au" #'undo-tree-visualize
 "-"  #'quick-calc
 "\\"   #'my-project-hydra|main)

Buffer / Bookmark

(map!
 :leader
 (:prefix-map ("b" . "buffer / bookmark")
  :desc "Save all buffers" "S" #'evil-write-all
  :desc "Rename Buffer"    "r" #'rename-buffer
  :desc "Rename Buffer"    "+" #'persp-add-buffer
  :desc "Rename Buffer"    "a" #'bbookmarks|save))

Code

(map!
 :leader
 (:prefix-map ("c" . "Code")
  :desc "Compile" "c"   #'my-project-compile|compile
  :desc "Rename" "r"   #'lsp-rename
  :desc "Execute code action" "a"   #'lsp-execute-code-action

  :desc "New AI Chat" "n" #'my-gptel|new
  :desc "Switch to AI Chat" "s" #'gptel
  :desc "Set gpt prompt" "p" #'gptel-system-prompt
  :desc "Abort current gpt chat" "x" #'gptel-abort
  :desc "Switch to last AI Chat" "l" #'my-gpel|select-last-buffer
  :desc "Add context" "+" #'gptel-context-add
  :desc "Remove context" "-" #'gptel-context-remove-all))

File / Goto

(map!
 :leader
 (:prefix-map ("f" . "file")
  :desc "Open Private Config" "P" (cmd! (find-file (f-join doom-private-dir "config.org")))
  (:prefix-map ("g" . "goto")
   :desc "Drive"                 "/" #'my-counsel|mounted-drives
   :desc "Desktop"               "D" (cmd! (find-file "~/Desktop"))
   :desc "Config"                "." (cmd! (find-file "~/.config"))
   :desc "Code"                  "c" (cmd! (find-file "~/Code"))
   :desc "Last captured"         "C" (cmd! (org-goto-marker-or-bmk org-capture-last-stored-marker))
   :desc "Downloads"             "d" (cmd! (find-file "~/Downloads"))
   :desc "Elfeed"                "e" (cmd! (find-file (car rmh-elfeed-org-files)))
   :desc "Media"                 "m" (cmd! (find-file "~/Media"))
   :desc "Music"                 "M" (cmd! (find-file "~/Media/Music"))
   :desc "Notes"                 "n" (cmd! (find-file org-directory))
   :desc "Project Root"          "p" (cmd! (find-file (projectile-project-root)))
   :desc "Last refiled"          "r" (cmd! (org-refile-goto-last-stored))
   :desc "Tmp"                   "t" (cmd! (find-file "/tmp"))
   :desc "Home"                  "h" (cmd! (find-file "~"))
   :desc "Run (Mounted Drives)"  "r" #'my-dired|find-mounted-drive)))

Git

(map!
 :leader
 (:prefix-map ("g" . "git")
  :desc "Amend Commit (No Message)"           "A" (cmd! (magit-commit-amend "--no-edit"))
  :desc "Blame"                               "B" #'magit-blame
  :desc "Changed Files"                       "F" #'my-magit|counsel-changed-files
  :desc "New Branch"                          "N" #'magit-branch-spinoff
  :desc "Show revision original File"         "O" #'my-magit|show-revision-original
  :desc "Stage File"                          "S" #'magit-stage-buffer-file
  :desc "Map-editor Changed Files"            "T" (cmd! (my-magit|counsel-changed-files "map-editor"))
  :desc "Amend Commit"                        "a" #'magit-commit-amend
  :desc "Checkout"                            "b" #'magit-checkout
  :desc "Diff"                                "d" #'magit-diff
  :desc "Push"                                "p" #'magit-push
  :desc "Undo"                                "u" #'my-magit|git-undo
  :desc "Worktree Popup"                      "w" #'magit-worktree
  :desc "New Org Pr"                          "!" #'+MM|new-pr-from-branch
  (:prefix ("f" . "file")
   :desc "VC file"                            "F" #'vc-revision-other-window)
  (:prefix ("l" . "list")
   :desc "List gists"                         "g" #'+gist:list
   :desc "List submodules"                    "n" #'magit-list-submodules
   :desc "List issues"                        "p" #'forge-list-issues
   :desc "List pull requests"                 "r" #'forge-list-pullreqs
   :desc "List pull awaiting review requests" "R" #'forge-list-requested-reviews
   :desc "List notifications"                 "s" #'forge-list-notifications)))

Insert

(map!
 :leader
 (:prefix-map ("i" . "insert")
  :desc "New Snippet"    "S" #'+snippets/new
  :desc "Killring"       "y" #'helm-show-kill-ring
  :desc "Comment Header" "h" #'my-comment-header|insert
  :desc "Date" "d"           #'my-date|insert-today-mode-specific))

Narrow

(map!
 :leader
 :desc "Widen" "=" #'widen
 :desc "Narow" "+" #'narrow-to-region)

Open

(map!
 :leader
 (:desc "open" :prefix "o"
   :desc "Calc"            :g "c" #'calc
   :desc "Calc"            :g "C" #'=calendar
   :desc "Elfeed"          :g "e" #'+eshell/toggle
   :desc "Eshell"          :g "E" #'my-elfeed|open
   :desc "Irc"             :g "i" #'=irc
   :desc "Mail"            :g "m" #'=mu4e
   :desc "Proced"          :g "p" #'proced
   :desc "Snippet"         :g "s" #'+snippets/edit
   :desc "Flycheck Errors" :g "$" #'flycheck-list-errors))

Projects

(map!
 :leader
 (:prefix-map ("p" . "project")
  :desc "Workspace Project Files" "P" #'my-workspaces|find-workspace-project-file
  :desc "Project VC"              "v" #'my-workspaces|workspace-project-vc
  :desc "Project Bookmarks"       "RET" #'my-jumpy|launch
  :desc "Compile"                 "c" #'bbuild|list
  :desc "Recompile"               "C" #'recompile))

Notes

(map!
 :leader
 (:prefix-map ("n" . "Notes")
  :desc "Save All Org Buffers"       "S" #'org-save-all-org-buffers
  :desc "Save All Org Buffers"       "s" #'+org/rg-notes
  :desc "Search"                     "f" #'+org|search
  :desc "Find in notes"              "n" #'+org|search-in-notes
  :desc "Agenda"                     "a" #'org-agenda
  :desc "Store Link"                 "y" #'org-store-link
  :desc "Store Link"                 "j" #'+private-journal|new-entry
  :desc "Visit Entry"              "SPC" #'my-org-indirect|clock-visit-entry
  :desc "Docs"                       "d" #'+org|search-programming-docs
  :desc "Projects"                   "p" #'+org|search-projects

  (:prefix-map ("b" . "Bookmarks")
   :desc "Goto Bookmarks File"       "b" (cmd! (find-file my-org/bookmarks-file))
   :desc "Search bookmarks"          "s" #'helm-org-pinboard)

  (:prefix-map ("r" . "Roam")
   ;; Create journal entry, but dont open roam link popup
   :desc "Journal: New Entry" "j"     (cmd! (let ((+org-roam-open-buffer-on-find-file nil))
                                              (call-interactively #'org-journal-new-entry)))
   :desc "List (Main)" "l" #'org-roam
   :desc "Insert" "i" (cmd! (+evil-org/normal-mode-paste-fix #'org-roam-insert))
   :desc "Switch to Buffer" "b" #'org-roam-switch-to-buffer
   :desc "Find File" "f" #'org-roam-find-file
   :desc "Show Graph" "g" #'org-roam-show-graph
   :desc "Capture" "c" #'org-roam-capture)

  ;; special goto locations I often visit
  :desc "Inbox"                      "i" (cmd! (+org/find-in-files "Main/inbox.org"))
  :desc "Work"                       "w" (cmd! (+org/find-in-files "Work/work.org"))

  (:prefix-map ("g" . "Goto")
   :desc "Goto Bookmarks File"       "b" (cmd! (find-file my-org/bookmarks-file))
   :desc "Inbox"                     "i" (cmd! (+org/find-in-files "Main/inbox.org"))
   :desc "Work"                      "w" (cmd! (+org/find-in-files "Work/work.org"))
   :desc "Work"                      "g" (cmd! (+org/find-in-files "Work/GTD.org"))
   :desc "Inbox"                     "i" (cmd! (+org/find-in-files "Main/inbox.org"))
   :desc "Projects"                  "p" (cmd! (+org/find-in-files "Main/projects.org"))
   :desc "Shoppinglist"              "s" (cmd! (+org/find-in-files "Main/shoppinglist.org"))
   :desc "Capture Last Stored"       "c" #'org-capture-goto-last-stored
   :desc "Refile Last Stored"        "r" #'org-refile-goto-last-stored)

  (:prefix-map ("c" . "clock")
   :desc "Timestamp Down"           "-"   #'org-clock-timestamps-down
   :desc "Timestamp Up"             "="   #'org-clock-timestamps-up
   :desc "Clock Out"                "C"   #'org-clock-out
   :desc "Goto Select"              "G"   (cmd! (org-clock-goto 'select))
   :desc "Clock In"                 "c"   #'org-clock-in
   :desc "Mark Default Task"        "d"   #'org-clock-mark-default-task
   :desc "Modify Effort Estimate"   "e"   #'org-clock-modify-effort-estimate
   :desc "Goto Current"             "g"   #'counsel-org-clock-history
   :desc "Resolve"                  "r"   #'org-resolve-clocks
   :desc "Clock In Last"            "l"   #'org-clock-in-last
   :desc "Clock In Last"            "o"   #'+org|clocked-visit-link
   :desc "Cancel"                   "x"   #'org-clock-cancel
   :desc "Visit Entry"              "SPC" #'my-org-indirect|clock-visit-entry
   :desc "Visit Entry"              "'"   #'my-org-indirect|clock-visit-entry)

  (:prefix-map ("q" . "Query")
   (:prefix-map ("w" . "Work")
    :desc "Projects: Work"            "w" #'+org-ql|work-projects
    :desc "Projects: Work"            "s" #'+org|work-search)
   :desc "Projects: Personal"        "p" #'+org-ql|projects)))

Search

(map!
 :leader
 (:prefix-map ("s" . "search")
  :desc "Imenu" "i" (cmd!
                     (pcase major-mode
                       ('stylus-mode (call-interactively #'my-stylus|imenu-top-level-classes))
                       (_ (call-interactively #'counsel-imenu))))
  :desc "Search Marks" "r" #'evil-show-marks
  :desc "Project for symbol" "P" #'+default/search-project-for-symbol-at-point
  :desc "Occur" "o" #'occur
  :desc "Web Lookup" "w" #'+lookup/online
  :desc "Web Lookup with prompt" "W" #'+lookup/online-select
  :desc "Search comment headers" "h" #'my-comment-header|counsel))

Toggle

(map!
 :leader
 (:prefix-map ("t" . "toggle")
  :desc "Comment Header Fold"        "<" #'my-comment-header|fold-all
  :desc "Auto Completion"            "c" #'+company/toggle-auto-completion
  :desc "Theme Dark/Light"           "t" #'my-ui|toggle-theme
  :desc "Visal Fill Column"          "o" #'+org|toggle-visual-wrap
  :desc "Visal Fill Column (Center)" "O" (cmd! (+org|toggle-visual-wrap))))

Window

(map!
 :leader
 :desc  "Split Vertical"        "|"    #'evil-window-vsplit
 :desc  "Split Horizontal"      "-"    #'evil-window-split

 (:prefix-map ("w" . "window")
  :desc  "Delete"               "d"    #'delete-window
  :desc  "Ace Delete Windo"     "D"    #'ace-delete-window
  :desc  "New"                  "n"    #'evil-window-new

  :desc  "Undo"                 "u"    #'winner-undo
  :desc  "Redo"                 "r"    #'winner-redo
  :desc  "Enlargen"             "o"    #'doom/window-enlargen

  :desc  "Toggle Split"         "T"    #'+window|toggle-split-direction

  :desc  "Split Vertical"       "|"    #'evil-window-vsplit
  :desc  "Split Horizontal"     "_"    #'evil-window-split

  :desc  "Move Left"            "H"    #'+evil/window-move-left
  :desc  "Move Down"            "U"    #'+evil/window-move-down
  :desc  "Move Up"              "K"    #'+evil/window-move-up
  :desc  "Move Right"           "L"    #'+evil/window-move-right

  :desc  "Set Height"           "C-_"  #'evil-window-set-height
  :desc  "Set Height"           "C-|"  #'evil-window-set-width

  :desc  "Swap"                 "SPC"  #'ace-swap-window
  :desc  "Toggle Locked"        "#"    #'my|toggle-window-dedicated
  :desc "Toggle Locked"         "."    #'+popup/raise))

Work

(map!
 :leader
 (:prefix-map ("0" . "Work")
  :desc "Open dev terminatl" "0" #'my-pitch|start-dev-terminal
  :desc "Format buffer"      "f" #'my-pitch|format-buffer))

Workspace

(map!
 :leader
 (:prefix-map ("TAB" . "workspace")
  :desc "Switch to"     "." #'my-workspaces/switch-to
  :desc "Close others"  "o" #'my-workspaces|close-others
  :desc "Create"        "c" #'my-workspaces/new-named
  :desc "Rename"        "," #'+workspace/rename
  :desc "Project Files" "p" #'my-workspaces|find-workspace-project-file
  :desc "Project VC"    "v" #'my-workspaces|workspace-project-vc
  :desc "Clone"         "C" (cmd!
                             (+workspace/new (format "Clone: %s" (+workspace-current-name)) t)
                             (message "Cloned current workspace %s" (+workspace-current-name)))
  :desc "Switch to last workspace" "0"   #'+workspace/other))

Yank

(defun +yank/execute-action (x &optional insert)
  "Copy the the yank or insert it during insert mode."
  (interactive)
  (cond
   ((or (evil-insert-state-p) insert) (insert x))
   (t (my/kill-and-message x)))
  x)

(defun +yank/dired-path ()
  "Returns the current dired entry or the buffer directory."
  (or (dired-file-name-at-point) dired-directory))

(defun +yank/buffer-filename ()
  "Returns the current buffers file name if possible."
  (cond ((doom-dired-buffer-p (current-buffer)) (+yank/dired-path))
        (buffer-file-name buffer-file-name)
        (org-src-source-file-name org-src-source-file-name)
        (t nil)))

(defun +yank/buffer-path ()
  "Returns the current buffers path."
  (or (+yank/buffer-filename) default-directory))

(defun +yank|filename (&optional insert)
  "Yank the buffer file name."
  (interactive "P")
  (or
   (-some->> (+yank/buffer-filename)
     ;; When pointing at a directory at dired, I still take the directory
     ((lambda (x) (if (f-file? x)
                      (concat (f-base x) "." (f-ext x))
                    (f-base (file-name-directory x)))))
     ((lambda (x) (+yank/execute-action x insert))))
   (user-error "Error: Not a file bufer!")))

(defun +yank|base (&optional insert)
  "Yank the buffer files base name."
  (interactive "P")
  (or
   (-some->> (+yank/buffer-filename)
     (file-name-base)
     ((lambda (x) (+yank/execute-action x insert))))
   (user-error "Error: Not a file bufer!")))

(defun +yank|directory (&optional insert)
  "Yank the buffer files directory."
  (interactive "P")
  (or
   (-some->> (+yank/buffer-path)
     (file-name-directory)
     ((lambda (x) (+yank/execute-action x insert))))
   (user-error "Error: Not a file bufer!")))

(defun +yank|path (&optional insert)
  "Yank the buffer files path."
  (interactive "P")
  (or
   (-some->> (+yank/buffer-path)
     ((lambda (x) (+yank/execute-action x insert))))
   (user-error "Error: Not a file bufer!")))

(defun +yank|relative-to-project (&optional insert)
  "Yank the buffer path relative to the projectile-project-root."
  (interactive "P")
  (or
   (-some->> (+yank/buffer-path)
     (f-full)
     ((lambda (x)
        (or
         (->> (s-replace (projectile-project-root) "" x)
              (-id-when #'s-present?))
         x)))
     ((lambda (x) (+yank/execute-action x insert))))
   (user-error "Error: Not a file bufer!")))

(map!
 :leader
 (:prefix-map ("y" . "Yank")
  :desc "filename"            "f" #'+yank|filename
  :desc "base"                "b" #'+yank|base
  :desc "directory"           "d" #'+yank|directory
  :desc "path"                "p" #'+yank|path
  :desc "path"                "y" #'+yank|path
  :desc "relative to project" "r" #'+yank|relative-to-project))

Local-Leader

Eshell

(defun my-eshell/init-keymap ()
  "Setup additional custom eshell keybindings to already existing doom bindings. This must be done in a hook because eshell-mode
redefines its keys every time `eshell-mode' is enabled."
  (map! :map eshell-mode-map
        :in "C-p"         #'eshell-previous-input
        :in "C-n"         #'eshell-next-input

        :localleader "l"  #'eshell/clear))

(add-hook 'eshell-first-time-mode-hook #'my-eshell/init-keymap)

Elisp

(map! :map emacs-lisp-mode-map
      :n "s-k"   (cmd! (sp-transpose-sexp)
                     (evil-previous-line))
      :n "s-j"   (cmd! (sp-push-hybrid-sexp)
                     (evil-next-line))
      :n "s-r" #'eval-buffer
      :n "g]"  #'sp-slurp-hybrid-sexp
      :n "g["  #'sp-forward-barf-sexp
      :localleader
      :desc "Jump to tangled source"  "j"  #'org-babel-tangle-jump-to-org
      :desc "Raise sexp"              "<" #'raise-sexp
      :desc "Barf Sexp"               ">" #'barf-sexp)

Clojure / Clojurescript

(map! :map clojurescript-mode-map
      :localleader
      (:prefix ("d" . "debug")
       :desc "Add debug statement" "a" #'my-pitch|debug-statement
       :desc "Add debug statement" "d" #'my-pitch|eval-debug-statement))

(map! :map (clojure-mode-map clojurescript-mode-map)
      :localleader
      (:prefix ("i" . "insert")
       :desc "Namespace" "n" #'my-clojure|update-ns-dwim))

Haskell

(map! :map haskell-mode-map
      :localleader
      :desc "Stylish" "s" #'my-haskell|autofix)

Bash

(map! :map sh-mode-map
      :localleader
      :desc "Eval Region"  "e" #'sh-execute-region
      :desc "Eval Region"  "E" #'executable-interpret)

Git

(map! :map magit-mode-map
      :localleader
      :desc "Toggle Magit Buffer Lock" "#" #'magit-toggle-buffer-lock)

Reasonml

(map! :map reason-mode-map
      :localleader
      :desc "Eval Region"  "r" #'refmt)

Mail

(map! :map (mu4e-view-mode-map mu4e-headers-mode-map)
      :localleader
      :g "x" (cmd!
              (require 'org-mu4e)
              (org-mu4e-store-and-capture)))

mail

(map! :map (mu4e-view-mode-map mu4e-headers-mode-map)
      :localleader
      :g "x" (cmd!
              (require 'org-mu4e)
              (org-mu4e-store-and-capture)))

Images

(map! :map 'image-mode-map
      :desc "Zoom in" :gn "+" #'image-increase-size
      :desc "Zoom in" :gn "-" #'image-decrease-size
      :desc "Zoom in" :gn "0" (cmd! (image-transform-set-scale 1)))

Wgrep Mode

(map! :map 'wgrep-mode-map
      :localleader
      :desc "Occur" :gn "o" #'my-ivy-occur|evil-mutliedit-last-search
      :desc "Occur" :gn "s" #'my-ivy-occur|evil-ex-last-search)

Org Mode

(map! :map org-mode-map
      :localleader
      :desc "Copy Buffer To Markdown"  "Y" #'my-org|copy-buffer-as-markdown
      :desc "Fetch url title"          "u" #'my-org|fetch-this-org-link-title
      :desc "Fetch rss feed from link" "r" #'my-org|fetch-this-org-link-rss
      (:prefix-map ("i" . "Insert")
       :desc "Created Property"        "c" #'my-org|add-created-property
       :desc "Attachment"              "a" #'my-attach|dwim
       :desc "Last download"           "d" #'my-attach|last-download))

Org Mode Agenda

(map! :map org-agenda-mode-map
      :localleader
      :desc "Fetch url title" "u" #'my-org-agenda|fetch-this-org-link-title)

Minibuffer

Copy and Paste from the minibuffer

Since the minibuffer has no evil mode, i’ve got these bindings to help out:

  • M-c: Copy the minibuffer line
  • M-v: Paste from clipboard to minibuffer (Same as C-r 0) This also removes trailing newlines
(defun my-evil|paste-pruned ()
  "Paste the current clipboard pruned from newlines"
  (interactive)
  (insert (s-trim (shell-command-to-string "pbpaste")))
  (doom/forward-to-last-non-comment-or-eol))

(defun my-minibuffer|copy-line ()
  "Copies the minibuffer content to the clipboard"
  (interactive)
  (save-excursion
    (doom/forward-to-last-non-comment-or-eol)
    (set-mark-command nil)
    (doom/backward-to-bol-or-indent)
    (kill-ring-save (mark) (point))))

Bindings

(defun my-minibuffer/setup-bindings ()
  "Set up keybindings for the minibuffer"
  (local-set-key (kbd "s-v") 'my-evil|paste-pruned)
  (local-set-key (kbd "s-c") 'my-minibuffer|copy-line)
  (local-set-key (kbd "M-/") 'hippie-expand))

(add-hook 'minibuffer-setup-hook 'my-minibuffer/setup-bindings)

Hacks

Override default browser

Why does this not work out of the box?

(defun browse-url-brave (url &optional _new-window)
  "Ask the Chromium WWW browser to load URL.
Default to the URL around or before point.  The strings in
variable `browse-url-chromium-arguments' are also passed to
Chromium.
The optional argument NEW-WINDOW is not used."
  (interactive (browse-url-interactive-arg "URL: "))
  (setq url (browse-url-encode-url url))
  (let* ((process-environment (browse-url-process-environment)))
    (apply #'start-process
           (concat "brave " url) nil
           "brave"
           (append
            browse-url-chromium-arguments
            (list url)))))

(pcase (getenv "BROWSER")
  ("brave" (setq browse-url-browser-function 'browse-url-brave))
  ("chromium" (setq browse-url-browser-function 'browse-url-chromium))
  ("firefox" (setq browse-url-browser-function 'browse-url-firefox)))

Fix evil-org-mode-map

Fixes (void-function ad-Advice-newline-and-indent) error for now. I honestly don’t know where this comes from.

(map!
 :map evil-org-mode-map
 :after org
 :i [return] #'org-return-and-maybe-indent)

Prevent woman defvaralias error

I’m using woman for manuals, but it throws this error on first launch

Warning (defvaralias): Overwriting value of ‘woman-topic-history’ by aliasing to ‘Man-topic-history’

Solution from here: Doom Discord question

(add-to-list 'warning-suppress-types '(defvaralias))

Stupid workaround where splitting a window with evil-ex-results breaks emacs…

(advice-add 'evil-window-vsplit :after #'evil-force-normal-state)
(advice-add 'evil-window-split :after #'evil-force-normal-state)

Emacs 28 Head Workaround

(with-no-warnings
      (defclass xref-location () ()
        :documentation "(Obsolete) location represents a position in a file or buffer."))

Make optional

(defmacro define-obsolete-function-alias ( obsolete-name current-name &optional when
                                           docstring)
  "Set OBSOLETE-NAME's function definition to CURRENT-NAME and mark it obsolete.

\(define-obsolete-function-alias \\='old-fun \\='new-fun \"22.1\" \"old-fun's doc.\")

is equivalent to the following two lines of code:

\(defalias \\='old-fun \\='new-fun \"old-fun's doc.\")
\(make-obsolete \\='old-fun \\='new-fun \"22.1\")

WHEN should be a string indicating when the function was first
made obsolete, for example a date or a release number.

See the docstrings of `defalias' and `make-obsolete' for more details."
  (declare (doc-string 4))
  `(progn
     (defalias ,obsolete-name ,current-name ,docstring)
     (make-obsolete ,obsolete-name ,current-name ,when)))

Fix Flycheck for emacs 29

(defun +flycheck/fix-emacs-29-save (&rest args)
  (flycheck-clear)
  (flycheck-buffer))

(advice-add #'flycheck-handle-save :after #'+flycheck/fix-emacs-29-save)

Evil Keys Fix

Fix remapping using hyper

(setq x-hyper-keysym 'ctrl)

Temp

(defun my-dired-unzip-and-convert-cbz-to-pdf ()
  "Unzip a CBZ file and convert its extracted JPG files to PDF asynchronously."
  (interactive)
  (let ((cbz-file (dired-get-filename)))
    (when (and cbz-file (string-match "\\.cbz$" cbz-file))
      (setq cbz-dir (file-name-sans-extension cbz-file))
      (async-shell-command (format "unzip %s -d %s && convert %s/**/*.jpg %s.pdf"
                                   (shell-quote-argument cbz-file)
                                   (shell-quote-argument cbz-dir)
                                   (shell-quote-argument cbz-dir)
                                   (shell-quote-argument cbz-dir))
                           (get-buffer-create "*CBZ Unzip and Convert Output*"))
      (message "CBZ file extraction and conversion initiated.")
      (notifications-notify :title "CBZ Conversion" :body "Conversion process initiated." :timeout 2000))))
(defun org-web/ensure-css-import ()
  (save-excursion
    (when (not (org-web/css-import?))
      (org-web/insert-css-require))))

(defun org-web|make-class ()
  (interactive)
  (when-let ((var-name (symbol-at-point)))
    (or (my-comment-header/goto-headers "Styles")
        (progn
          (goto-char (point-min))
          (evil-jump-item)
          (forward-line)
          (my-comment-header|insert "Styles")))
    (end-of-line)
    (forward-line)
    (when (my-comment-header|goto-next-comment-header)
      (previous-line))
    (insert "\n")
    (insert (t! "(css <<var-name>> [] \n     \"\")\n"))
    (backward-char 3)
    (evil-insert-state))
  (org-web/ensure-css-import))