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.
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.
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
: Variablemy/function
: Private functionmy-namespace/function
: Private function with namespace
my|function
: Interactive functionmy-namespace|function
: Interactive function
my@function
: Macromy*hook
: Hook function
There are only a few exceptions, which I’m keeping without the prefix for ease of writing/reading:
- Dash Extensions
Prefixed with a
-
- Template Literals
template
ort
macros
;; -*- lexical-binding: t -*-
;; -*- no-byte-compile: t; -*-
;; -*- no-byte-compile: t; -*-
(package! ct)
(require 'dash)
(require 's)
(require 'f)
(require 'noflet)
(require 'ct)
(require 'dash)
(require 's)
(require 'f)
(require 'noflet)
(require 'ct)
(-some->> (f-expand "~/.config/dotfiles/new/modules/scripts/src/hyma.el")
(-id-when #'f-exists?)
(load-file))
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
(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)
;;;###autoload
(defmacro my@ignore-args (fn)
"Return function that ignores its arguments and invokes FN."
`(lambda (&rest _rest)
(funcall ,fn)))
Similar to the clojure comment
form
(defmacro comment (&rest body)
"Comment out one or more s-expressions."
nil)
;;;###autoload
(defun my/noop (&optional args) nil)
;;;###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)) ()))
;;;###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))
(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))))
(defun my-buffer/contains-line (string)
(save-excursion
(save-match-data
(goto-char (point-min))
(search-forward string nil t))))
(defun my-buffer/empty-line? ()
(-> (thing-at-point 'line)
(substring-no-properties)
(s-trim)
(equal "")))
;;;###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))
;;;###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)))
;;;###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)))))
(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 ""))))
;;;###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))))
(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))))
(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))))
(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 "/")))))
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))))
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)))
(defun my-shell/command-to-list (cmd)
"Split output from shell-command to list"
(split-string (shell-command-to-string cmd) "\n" t))
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)))
;;;###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 "\""))))
(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"))
(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)
;;;###autoload
(defun my-file/timestamp (path)
(->> (file-attributes path)
(nth 5)))
;;;###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)))
;;;###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)))
(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)))))
(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)))
(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?)))
;;;###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))
(defun my/bool-to-enabled-string (x)
"Convert bool X to string for messaging.
t -> \"Enabled\")
nil -> \"Disabled\""
(if x "Enabled" "Disabled"))
(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))>>")))
(with-eval-after-load 'dash
(--each
(list '-> '->> '-some-> '-some->> '--each '-each '-map '--map)
(function-put it 'lisp-indent-function nil)))
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 the current input without breaking the pipe.
;;;###autoload
(defun -log (x)
"Function docstring"
(--tap (message "%s" it) x))
;;;###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)))
;;;###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)))
;;;###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))
(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)
;;;###autoload
(defun -f-join (x path)
"Reversed argument order for f-join"
(f-join path x))
;;;###autoload
(defun -f-split-ext (s)
(list (f-no-ext s) (f-ext s)))
(assert (equal (-f-split-ext "foo_bar.clj") '("foo_bar" "clj")))
;;;###autoload
(defun -f-map-filename (fn s)
(concat (funcall fn (f-no-ext s))
(-some->> (f-ext s) (concat "."))))
(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"))
;;;###autoload
(defun f-tildify (path)
"Replace the HOME directory in path"
(s-replace-regexp (t! "^<<(getenv \"HOME\")>>") "~" path))
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))))
(defun -plist-get (plist prop)
(plist-get prop plist))
(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))
(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))))
(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)))))
Porting over clojure-like functions as I’m more used to them
(defun prn (&rest args)
(let* ((str (->> (-map (lambda (_) "%s") args)
(s-join " ")))
(msg-args (-concat (list str) args)))
(apply #'message msg-args)))
(defun doto (x fn)
(funcall fn x)
x)
(defun doto-last (fn x)
(funcall fn x)
x)
(defun nil? (x)
(null x))
(comment
(nil? nil)
(nil? t)
nil)
(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)
(defun inc (n)
(+ 1 (or n)))
(comment
(inc nil)
(inc 0)
nil)
(defun some? (x)
(not (nil? x)))
(comment
(some? 1)
(some? nil)
nil)
(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"))))
(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))
Emacs implmenentation for my bookmarks management written in clojure:
Command key | Description |
---|---|
:org-goto "Heading Regexp" | Go to an org heading within the file |
:org-narrow-indirect | Narrow resulting org buffer indirectly |
(package! parseedn)
(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")
(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)))
(map!
:leader
"RET" #'bbookmarks|list)
(map!
:leader
(:prefix-map ("j" . "jumpy")
:desc "Project" "p" #'bbookmarks|list-project-bookmarks))
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))))
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)))))
(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)))
(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)))
(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)))
(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))))
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)))))
;; 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))
(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))))))))
(defun my-project-hydra|main ()
(interactive)
(pcase (projectile-project-name)
("org-web" (my-project-hydra|org-web/body))
(_ (user-error "No project hydra found."))))
(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"))
(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))
(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)
Self-written simpler alternative to emacs-gitmoji
(defvar penpot-gitmoji
(ht
(":bug:"
(ht ('emoji "🐛")
('entity "🐛")
('code ":bug:")
('description "A commit that fixes a bug.")
('name "bug")
('semver nil)))
(":sparkles:"
(ht ('emoji "✨")
('entity "✨")
('code ":sparkles:")
('description "A commit that an improvement.")
('name "sparkles")
('semver nil)))
(":tada:"
(ht ('emoji "🎉")
('entity "🎉")
('code ":tada:")
('description "A commit with new feature.")
('name "tada")
('semver nil)))
(":recycle:"
(ht ('emoji "♻️")
('entity "♲")
('code ":recycle:")
('description "A commit that introduces a refactor.")
('name "recycle")
('semver nil)))
(":lipstick:"
(ht ('emoji "💄")
('entity "💄")
('code ":lipstick:")
('description "A commit with cosmetic changes.")
('name "lipstick")
('semver nil)))
(":ambulance:"
(ht ('emoji "🚑")
('entity "🚑")
('code ":ambulance:")
('description "A commit that fixes critical bug.")
('name "ambulance")
('semver nil)))
(":books:"
(ht ('emoji "📚")
('entity "📚")
('code ":books:")
('description "A commit that improves or adds documentation.")
('name "books")
('semver nil)))
(":construction:"
(ht ('emoji "🚧")
('entity "🚧")
('code ":construction:")
('description "A WIP commit.")
('name "construction")
('semver nil)))
(":boom:"
(ht ('emoji "💥")
('entity "💥")
('code ":boom:")
('description "A commit with breaking changes.")
('name "boom")
('semver nil)))
(":wrench:"
(ht ('emoji "🔧")
('entity "🔧")
('code ":wrench:")
('description "A commit for config updates.")
('name "wrench")
('semver nil)))
(":zap:"
(ht ('emoji "⚡")
('entity "⚡")
('code ":zap:")
('description "A commit with performance improvements.")
('name "zap")
('semver nil)))
(":whale:"
(ht ('emoji "🐳")
('entity "🐳")
('code ":whale:")
('description "A commit for docker-related stuff.")
('name "whale")
('semver nil)))
(":rewind:"
(ht ('emoji "⏪")
('entity "⏪")
('code ":rewind:")
('description "A commit that reverts changes.")
('name "rewind")
('semver nil)))
(":paperclip:"
(ht ('emoji "📎")
('entity "📎")
('code ":paperclip:")
('description "A commit with other not relevant changes.")
('name "paperclip")
('semver nil)))
(":arrow_up:"
(ht ('emoji "⬆️")
('entity "⬆")
('code ":arrow_up:")
('description "A commit with dependencies updates.")
('name "arrow_up")
('semver nil)))))
(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))))
(map!
:map git-commit-mode-map
:leader
(:prefix-map ("i" . "insert")
:desc "Gitmoji" "g" #'my-gitmoji|replace))
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))
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)
(defcustom my-directories:downloads-dir "~/Downloads"
"Downloads Directory"
:type 'string)
(defcustom my-directories:repositories-dir "~/Code/Repositories"
"Downloads Directory"
:type 'string)
(setq user-full-name "Florian Schrödl")
(setq
trash-directory "~/.Trash/"
delete-by-moving-to-trash t)
(setq tags-revert-without-query 1)
(blink-cursor-mode -1)
(setq blink-matching-paren nil)
(setq visible-cursor nil)
Variables that I want to safely set from .dir-locals
files.
(put '+file-templates-dir 'safe-local-variable #'stringp)
(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)
(add-to-list 'auth-sources "~/.config/gnupg/.authinfo.gpg")
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!
: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))
Set it to 32 MiB
.
(setq doom-gc-cons-threshold 33554432)
;;;###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))))))
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>>!"))))
;;;###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"))))
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))
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)))
(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-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)))))
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))
(add-hook 'doom-load-theme-hook #'*doom-themes-custom-set-faces)
(defun *doom-themes-custom-set-faces ()
(set-face-attribute 'fringe nil
:foreground (face-background 'default)
:background (face-background 'default))
(custom-set-faces!
'(whitespace-tab :background nil)
'(bookmark-face :background nil)
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))
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"))
Switch the highlight.
'(mu4e-highlight-face :inherit mu4e-unread-face)
Remove the ugly grey background
'(org-column :background nil)
))
(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))))
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))))
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)))
(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)
(setq-hook! 'prog-mode-hook show-trailing-whitespace nil)
Draw the underline at the bottom of the text, not at the end of line-spacing.
(setq x-underline-at-descent-line nil)
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)
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)))
EmacsWiki: Abbrev Mode Correcting Typos and Misspellings with Abbrev - Mastering Emacs
(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)
Packages that I haven’t yet moved to their structure.
(package! doom-snippets :ignore t)
(package! my-doom-snippets
:recipe (:host github
:repo "floscr/doom-snippets"
:files ("*.el" "*")))
(package! flycheck-posframe :pin "6eea204138721f511051af5138326fecaad237b7")
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)
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")
(package! lsp-ui :disable t)
(package! merlin :pin "e4791e22986993c36c3f5c91e8dff93494cc232e")
(package! merlin-eldoc :disable t)
(package! sqlite3)
Fix for missing package
s-replace-all: Symbol’s function definition is void: rxt-quote-pcre
(package! pcre2el)
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! noflet)
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))
(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)))
(package! doom-themes
:recipe (:host github :repo "floscr/emacs-doom-themes" :files ("*.el" "themes/*.el"))
:pin nil)
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))
(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)))
(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)))
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)))
(defun +workspace/new-named ()
"Create a new named workspace."
(interactive)
(let ((name (read-string "New workspace name: ")))
(if name (+workspace/new name))))
(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))))
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))
(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))))))
Used for evil-little-word.
(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! evil-replace-with-register)
(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! evil-text-objects-javascript :recipe (:host github :repo "urbint/evil-text-objects-javascript"))
(after! evil-snipe
(setq evil-snipe-repeat-keys t))
Filters out whitespace from the kill ring
(package! clean-kill-ring
:recipe (:type git
:host github
:repo "NicholasBHubbard/clean-kill-ring.el")
:pin "3338a186329a1bef19e6781aa75befa5a0d031be")
(use-package! clean-kill-ring
:config (clean-kill-ring-mode 1))
(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)))))
(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))))
(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))
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)
(package! literate-calc-mode)
(use-package! literate-calc-mode
:commands (literate-calc-mode literate-calc-minor-mode))
Distraction free writing
(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! narrow-indirect :recipe (:host github :repo "emacsmirror/narrow-indirect"))
(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))
(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)))
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)
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)
;; Set formatter to empty list which should be defined per mode
(setq +format-on-save-enabled-modes '())
(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))))
(defun my-sp/yank-sexp (&optional ARG)
"Yank the current sexp without killing it."
(interactive)
(sp-kill-sexp ARG t))
(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)
(after! (:and clojure-mode apheleia)
(my-formatters/remove-formatter 'cljfmt))
(after! (:and beancount-mode apheleia)
(my-formatters/remove-formatter 'bean-format))
(package! highlight-indent-guides :pin "cf352c85cd15dd18aa096ba9d9ab9b7ab493e8f6")
(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)))
(package! tree-sitter)
(package! tree-sitter-langs)
(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! cheat-sh)
(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! emmet-mode)
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)))
(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! rainbow-mode)
(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! impatient-mode)
(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))
(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! diff-lisp :recipe (:host github :repo "redguardtoo/diff-lisp"))
(use-package! diff-lisp)
Emacs database layer interface
(package! edbi)
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"))))
(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)))
(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! eslintd-fix)
(use-package! eslintd-fix
:after js2-mode
:config
(setq
flycheck-javascript-eslint-executable (executable-find "eslint_d")
flycheck-disabled-checkers '(javascript-jshint javascript)))
(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! js-import :recipe (:host github :repo "floscr/js-import"))
(put 'js-import-alias-map 'safe-local-variable (lambda (x) t))
(package! jest :recipe (:host github :repo "floscr/emacs-jest"))
(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! indium)
(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))
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! graphql)
(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)))
(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")))))))
(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)))))
I don’t need them with parinfer and lispyville, they’re just distracting.
(remove-hook 'emacs-lisp-mode-hook #'rainbow-delimiters-mode)
(map! :map emacs-lisp-mode-map
:desc "Eval" :n "RET" #'my-elisp|eval-last-sexp)
(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 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))
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)))
(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)
(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 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 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)))))
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))
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))))))))
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))
(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))
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)
(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))))
(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))))))
(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>>")))))
(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)))
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)))
(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))
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))
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))))
(package! nixpkgs-fmt)
(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))
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)
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)))
(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)))
(map! (:map markdown-mode-map
:n "<" #'markdown-promote
:n ">" #'markdown-demote))
(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)))
(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))))
(use-package! flycheck-clj-kondo
:when t
:after flycheck)
(package! hiccup-cli)
(use-package! hiccup-cli)
(package! cider :pin "810337cee931d9f14aa16550a74516f8337a47f3")
(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))
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")
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))
- 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))
(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)))
(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! lispyville :recipe (:host github :repo "noctuid/lispyville"))
(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))
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)
(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! jet :recipe (:host github :repo "ericdallo/jet.el"))
(use-package! jet)
(package! clj-refactor :disable t)
(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?)))
(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))))))
(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)))))))
(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")))))
(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))))))
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))))))
(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))))
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))))
(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)))
(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))))
(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"))))
(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))))
(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))))
(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))))
(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)
(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))))
(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:\\'"))))
(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))
(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))
(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")))))
(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)
(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!)
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))
(defun my-treesitter/node-point-eq (a b)
(= (ts-node-start-position a)
(ts-node-start-position b)))
(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)))))
(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)
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! gptel :pin "975c3e6")
(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)))
(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)))
(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))
(package! magit :pin "7dfebba55bf687a25049882c2316166d968048ea")
(package! compat :pin "9a234d0")
(package! transient :pin "3430943eaa3222cd2a487d4c102ec51e10e7e3c9")
(package! forge :pin "a63685b")
(package! ghub :pin "23d1cd6")
(package! elescope)
(use-package elescope
:commands (elescope-checkout)
:config
(setq elescope-root-folder my-directories:repositories-dir)
:init
(defalias '+git|clone 'elescope-checkout))
(package! git-lens)
(use-package! git-lens
:commands (git-lens))
(use-package! code-review
:config
(setq code-review-auth-login-marker 'forge))
(use-package browse-at-remote
:config
(add-to-list
'browse-at-remote-remote-type-regexps
'(:host "^gitea\\.florianschroedl\\.com$" :type "gitea")))
(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)
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))
(after! magit
(transient-append-suffix 'magit-worktree "b" '("c" "Create branch and worktree" my-magit|create-worktree-project-from-branch)))
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))))
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)))
(defun my-magit|git-undo ()
"Soft reset current git repo to HEAD~1."
(interactive)
(magit-reset-soft "HEAD~1"))
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))
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)>>")))
(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)))
(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))))
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)))))
(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)))
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 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)))))
(advice-add #'magit-toggle-buffer-lock :after (lambda () (my/bool-state-message 'magit-buffer-locked-p)))
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>>"))))
(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)))
(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)))))
(defun my-magit|merge-master ()
"Merges origin master to the current branch."
(interactive)
(magit-merge-plain "origin/master"))
(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
(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))))))
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)
(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))))))
(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")))
(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-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))
My workflow for navigating diffs
Use z1
to fold all diffs to their file headers and press’s {
or }
to
- Refold all sections
- Go to the next section
- 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))))
(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)
(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 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)
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)
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)))
Filter current dired buffer by query/extension etc.
Trigger with g/.
in normal-mode
.
(package! dired-filter)
(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))))))
(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)
(defconst my-dired:default-listing-switches
"-ahl -t --group-directories-first")
(setq dired-listing-switches my-dired:default-listing-switches)
(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$")))
(add-hook 'dired-mode-hook 'auto-revert-mode)
(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))))))
(after! async
(dired-async-mode 1))
(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)))
(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"))
(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))))
(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))
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)
(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))))))
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)))
(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"))
(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)))))))
(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))))
(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))))
(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)
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)))
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)
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)))
(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)
Explore disk usage in emacs
(package! disk-usage)
(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! atomic-chrome)
(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! daemons)
(use-package! daemons
:commands (daemons))
(package! proced-narrow)
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)
(map! :map proced-mode-map
:n "/" #'proced-narrow
:n "gr" #'proced)
(map! :map process-menu-mode-map
:n "gr" #'list-processes)
(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))))
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/")))
(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)))
(package! pdf-tools :built-in 'prefer)
;; Fix midnight colors for doom-one theme
(setq pdf-view-midnight-colors '("#BBC2CD" . "#282C34"))
(map!
:map pdf-occur-buffer-mode-map
:gn [tab] (cmd! (pdf-occur-goto-occurrence t)))
Major mode for reading EPUBs in Emacs
(package! nov)
(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))
(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))
Naive linter for english prose.
(use-package! writegood-mode
:hook 'nil)
(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 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)
(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)))))
(use-package! spell-fu
:general ([remap ispell-word] #'my-spell-correct|auto-correct))
Disable flycheck completely in org-src-mode, the hints are not useful.
(add-hook 'org-src-mode-hook (lambda () (flycheck-mode -1)))
(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))
(defun my-counsel|project-file-jump (x)
"Jump to file in project"
(interactive)
(counsel-file-jump nil (f-join (projectile-project-root) x)))
(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)))))
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)
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)))))
(setq ivy-read-action-function #'ivy-hydra-read-action)
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)
(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)
(map! :map ivy-occur-mode-map
:gni "RET" #'ivy-occur-press-and-switch)
Better sorting for company
(package! prescient)
(package! company-prescient)
(use-package! prescient
:config
(prescient-persist-mode 1))
(use-package! company-prescient
:after company
:config
(company-prescient-mode 1))
(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 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))
(setq company-transformers '(company-sort-by-occurrence)
company-idle-delay 0.5)
(package! lsp-mode :pin "dd61303b2dc989a58f7dddd4c754f828a3f52107")
(package! lsp-ui :pin "072bb29152038518c2478813b82c8d04d07df84c")
(package! lsp-ivy :pin "9ecf4dd9b1207109802bd1882aa621eb1c385106")
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)
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))
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)
(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))))
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)))
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))))
(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)))))
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))))
(use-package! yasnippet
:init
(require 'doom-snippets nil t))
(package! skeletor)
(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")))
(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))))
No need, screws with entry buffer position and header display…
(package! elfeed-goodies :disable t)
(defun my-elfeed|open ()
(interactive)
(unless (get-buffer "*elfeed-search*")
(setq elfeed-search-filter +rss:default-search-filter))
(elfeed)
(my-rss|hydra/body))
(defun my-rss/filter-by-unread ()
"Show elfeed articles tagged with unread"
(interactive)
(elfeed-search-set-filter "@6-months-ago +unread"))
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)))))
(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)))
(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)))
(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)
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)
(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"))))
(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))
(setq rmh-elfeed-org-files (list (+org/expand-org-file-name "Elfeed/Elfeed.org")))
These don’t work if you have a big line-height.
(setq +rss-enable-sliced-images nil)
(setq +rss:default-search-filter "@2-week-ago +unread -YOUTUBE")
(setq-default elfeed-search-filter +rss:default-search-filter)
(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"))))
(map!
:after elfeed
:map elfeed-search-mode-map
:gn "r" #'elfeed-update)
(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))
(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"))))
(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)
(setq shr-width 100)
(defun my-calendar::filtered|personal (&rest args)
(interactive)
(let ((org-agenda-skip-function '(+org/agenda-skip-without-match "-WORK-HIDE_CALENDAR")))
(call-interactively #'=calendar)))
(defun my-calendar::filtered|just-family (&rest args)
(interactive)
(let ((org-agenda-skip-function '(+org/agenda-skip-without-match "+FAMILY|+BIRTHDAY")))
(call-interactively #'=calendar)))
(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)))
(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)))
(defun my-calendar::filtered|work (&rest args)
(interactive)
(let ((org-agenda-skip-function '(+org/agenda-skip-without-match "+WORK-HIDE_CALENDAR")))
(call-interactively #'=calendar)))
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)))
(after! calfw
:config
(setq general-holidays
'((holiday-fixed 1 1 "New Years")
(holiday-fixed 5 1 "1st Mai"))))
(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"))))
(after! calfw
:config
(setq calendar-holidays
(append
general-holidays
austrian-holidays
holiday-solar-holidays)))
(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))
(setq math-additional-units '((GB "1024 * MiB" "Giga Byte")
(MB "1024 * KiB" "Mega Byte")
(KB "1024 * B" "Kilo Byte")
(B nil "Byte")))
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)
(defun my-eshell-cmd|groot ()
(interactive)
(setq default-directory (projectile-project-root)))
(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)))
(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! eat)
(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))
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
(set-popup-rule! "^\\*Org Indirect" :side 'bottom :size 0.35 :quit t :ttl nil :select t :autosave)
(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)
(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)
(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)))
(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)))))))
(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)))
(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))
(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)))
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)))
ob-async enables asynchronous execution of org-babel src blocks via the :async
property.
(use-package! ob-async
:after org-babel)
(setq org-roam-directory (+org/expand-org-file-name "Roam"))
(setq org-roam-db-location (+org/expand-org-file-name "org-roam.db"))
(use-package! org-protocol)
(package! ob-nix :recipe (:host codeberg :repo "theesm/ob-nix") :pin "76d71b37fb")
(use-package! ob-nix)
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)))
(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! ox-gfm)
(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)
Use with destructuring like this
(-let (((&plist* title) (my-org/link-at-point)))
title)
(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))))))
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)))
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))
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))))
;;;###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?))
(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))
(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))
- 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))
(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 "*")))
(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) "******"))
(defun my-dired|search-attach-at-point ()
(interactive)
(+ivy/project-search nil (f-base (dired-file-name-at-point)) (f-expand org-directory)))
(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)))
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)))))
- To display
webp
images - Emacs: viewing webp images
(setq image-use-external-converter t)
(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)))))
(map!
:map org-mode-map
:desc "Org Meta Return Dwim" :g "M-RET" #'my-org|meta-ret-dwim)
(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))))
(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)))
(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)))
(defun my-pitch/convert-hiccup-to-uix ()
(interactive)
(save-excursion
(clojure-convert-collection-to-list)
(forward-char 1)
(insert "$ ")))
(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>>)"))))
(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!")))))
(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)))
(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)))
(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)))
(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)))))
(defun my-pitch|ghub-create-pull-request ()
"Function docstring"
(interactive)
(shell-command "gh pr create --web"))
(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)))))
(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)))
(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)))
(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)))
(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)))
(package! chatgpt-shell :recipe (:host github :repo "xenodium/chatgpt-shell"))
(use-package! chatgpt-shell
:config
(setq chatgpt-shell-openai-key
(lambda ()
(-> (f-read "/run/agenix/openai")
(s-trim)))))
Keeping Secrets in Emacs with GnuPG and Auth Sources - Mastering Emacs
(fset 'epg-wait-for-status 'ignore)
(use-package! beancount
:config
(setq beancount-number-alignment-column 80)
(after! apheleia-core
(setq apheleia-formatters (delq (assoc 'bean-format apheleia-formatters) apheleia-formatters))))
(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 "'"))))))
Requires emacs 29
(package! listen :recipe (:host github :repo "alphapapa/listen.el"))
(use-package listen
:ensure t
:defer t
:commands (listen))
(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))))
(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)))
(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))
(map! :map my-org-web-mode-map
:localleader
(:prefix ("m" . "modify")
:desc "Make css class" "c" #'org-web|make-class))
(map! :g "C-±" #'+popup/raise)
(map! :nm "C-z" nil)
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")))
(map! :g "M-RET" #'lsp-find-references)
(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))
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)
q
for any type of quoteB
for curly bracesr
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))
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)
(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)
(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))
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))
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))
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)
(define-key minibuffer-local-map "\C-p" 'previous-history-element)
(define-key minibuffer-local-map "\C-n" 'next-history-element)
(map! :n "gb" #'evil-switch-to-windows-last-buffer)
(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)
(map! :m "-" #'my-buffer|jump-source-dwim)
(map! :map visual-line-mode-map
:nv "j" #'evil-next-visual-line
:nv "k" #'evil-previous-visual-line)
Insert Mode bindings, mostly unicode insertion and workaround for german umlaut.
(map! :i "A-y" #'helm-show-kill-ring)
(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 "ℹ")))
Global [
& ]
combinator bindings
(map!
:n "]F" #'dumb-jump-go
:n "[F" #'dumb-jump-back)
(map!
:n "]e" #'flycheck-next-error
:n "[e" #'flycheck-previous-error)
(map!
:n "]1" #'my-project|jump-to-or-create-test-file
:n "[1" #'my-project|jump-to-or-create-test-file)
(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)
Custom evil text objects mostly stolen from Spacemacs|define-text-object-regexp.
(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)))))
(+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" "-" "-")
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))
Extend the default evil ex commands from +commands.el
(after! evil-ex
:config
(evil-ex-define-cmd "W" #'evil-write))
(map! :nv "C-M-d" #'evil-multiedit-match-all)
(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))
(define-key! 'global
[remap evil-toggle-fold] #'my-comment-header|toggle-fold-or-fallback)
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)))
(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)
(map!
:leader
"'" #'+popup/toggle
"au" #'undo-tree-visualize
"-" #'quick-calc
"\\" #'my-project-hydra|main)
(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))
(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))
(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)))
(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)))
(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))
(map!
:leader
:desc "Widen" "=" #'widen
:desc "Narow" "+" #'narrow-to-region)
(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))
(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))
(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)))
(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))
(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))))
(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))
(map!
:leader
(:prefix-map ("0" . "Work")
:desc "Open dev terminatl" "0" #'my-pitch|start-dev-terminal
:desc "Format buffer" "f" #'my-pitch|format-buffer))
(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))
(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))
(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)
(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)
(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))
(map! :map haskell-mode-map
:localleader
:desc "Stylish" "s" #'my-haskell|autofix)
(map! :map sh-mode-map
:localleader
:desc "Eval Region" "e" #'sh-execute-region
:desc "Eval Region" "E" #'executable-interpret)
(map! :map magit-mode-map
:localleader
:desc "Toggle Magit Buffer Lock" "#" #'magit-toggle-buffer-lock)
(map! :map reason-mode-map
:localleader
:desc "Eval Region" "r" #'refmt)
(map! :map (mu4e-view-mode-map mu4e-headers-mode-map)
:localleader
:g "x" (cmd!
(require 'org-mu4e)
(org-mu4e-store-and-capture)))
(map! :map (mu4e-view-mode-map mu4e-headers-mode-map)
:localleader
:g "x" (cmd!
(require 'org-mu4e)
(org-mu4e-store-and-capture)))
(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)))
(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)
(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))
(map! :map org-agenda-mode-map
:localleader
:desc "Fetch url title" "u" #'my-org-agenda|fetch-this-org-link-title)
Since the minibuffer has no evil mode, i’ve got these bindings to help out:
M-c
: Copy the minibuffer lineM-v
: Paste from clipboard to minibuffer (Same asC-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))))
(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)
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)))
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)
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))
(advice-add 'evil-window-vsplit :after #'evil-force-normal-state)
(advice-add 'evil-window-split :after #'evil-force-normal-state)
(with-no-warnings
(defclass xref-location () ()
:documentation "(Obsolete) location represents a position in a file or buffer."))
(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)))
(defun +flycheck/fix-emacs-29-save (&rest args)
(flycheck-clear)
(flycheck-buffer))
(advice-add #'flycheck-handle-save :after #'+flycheck/fix-emacs-29-save)
Fix remapping using hyper
(setq x-hyper-keysym 'ctrl)
(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))