branch: elpa/crux commit 12c4901d85813a5308767fda5d46507e7ce68078 Author: Bozhidar Batsov <bozhi...@batsov.com> Commit: Bozhidar Batsov <bozhi...@batsov.com>
Initial commit --- README.md | 85 ++++++++++++++ crux.el | 373 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 458 insertions(+) diff --git a/README.md b/README.md new file mode 100644 index 0000000..d64dc89 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +[![License GPL 3][badge-license]][copying] +[![MELPA][melpa-badge]][melpa-package] +[![MELPA Stable][melpa-stable-badge]][melpa-stable-package] + +# crux + +A **C**ollection of **R**idiculously **U**seful e**X**tensions for Emacs. +crux bundles a few useful interactive commands to enhance your +overall Emacs experience. + +## Installation + +Available on all major `package.el` community maintained repos - +[MELPA Stable][] and [MELPA][] repos. + +MELPA Stable is recommended as it has the latest stable version. +MELPA has a development snapshot for users who don't mind breakage but +don't want to run from a git checkout. + +You can install `crux` using the following command: + +<kbd>M-x package-install [RET] crux [RET]</kbd> + +or if you'd rather keep it in your dotfiles: + +```el +(unless (package-installed-p 'crux) + (package-refresh-contents) + (package-install 'crux)) +``` + +If the installation doesn't work try refreshing the package list: + +<kbd>M-x package-refresh-contents</kbd> + +Add the following to your Emacs config to enable +`crux`: + +```el +(crux-setup-default-keybindings) +``` + +## Keybindings + +Here's the list of the default keybindings. Feel free to bind +individual commands to whatever keybindings you prefer. + +Keybinding | Description +-------------------|------------------------------------------------------------ +<kbd>C-c o</kbd> | Open the currently visited file with an external program. +<kbd>C-S-RET</kbd> or <kbd>Super-o</kbd> | Insert an empty line above the current line and indent it properly. +<kbd>S-RET</kbd> or <kbd>M-o</kbd> | Insert an empty line and indent it properly (as in most IDEs). +<kbd>C-c n</kbd> | Fix indentation in buffer and strip whitespace. +<kbd>C-c f</kbd> | Open recently visited file. +<kbd>C-M-\\</kbd> | Indent region (if selected) or the entire buffer. +<kbd>C-c u</kbd> | Open a new buffer containing the contents of URL. +<kbd>C-c e</kbd> | Eval a bit of Emacs Lisp code and replace it with its result. +<kbd>C-c s</kbd> | Swap two active windows. +<kbd>C-c D</kbd> | Delete current file and buffer. +<kbd>C-c r</kbd> | Rename the current buffer and its visiting file if any. +<kbd>C-c t</kbd> | Open a terminal emulator (`ansi-term`). +<kbd>C-c k</kbd> | Kill all open buffers except the one you're currently in. +<kbd>C-c TAB</kbd> | Indent and copy region to clipboard +<kbd>C-c I</kbd> | Open user's init file. +<kbd>C-c S</kbd> | Open shell's init file. +<kbd>Super-r</kbd> | Recent files +<kbd>Super-j</kbd> | Join lines +<kbd>Super-k</kbd> | Kill whole line +<kbd>C-Backspace</kbd> | Kill line backwards + +## License + +Copyright © 2015 Bozhidar Batsov and [contributors][]. + +Distributed under the GNU General Public License; type <kbd>C-h C-c</kbd> to view it. + +[badge-license]: https://img.shields.io/badge/license-GPL_3-green.svg +[melpa-badge]: http://melpa.org/packages/crux-badge.svg +[melpa-stable-badge]: http://stable.melpa.org/packages/crux-badge.svg +[melpa-package]: http://melpa.org/#/crux +[melpa-stable-package]: http://stable.melpa.org/#/crux +[COPYING]: http://www.gnu.org/copyleft/gpl.html +[contributors]: https://github.com/clojure-emacs/crux/contributors +[melpa]: http://melpa.org +[melpa stable]: http://stable.melpa.org diff --git a/crux.el b/crux.el new file mode 100644 index 0000000..95b299a --- /dev/null +++ b/crux.el @@ -0,0 +1,373 @@ +;;; crux --- A Collection of Ridiculously Useful eXtensions. +;; +;; Copyright © 2015 Bozhidar Batsov +;; +;; Author: Bozhidar Batsov <bozhi...@batsov.com> +;; URL: https://github.com/bbatsov/crux +;; Version: 0.1.0 +;; Keywords: convenience +;; Package-Requires: ((seq "1.11")) + +;; This file is not part of GNU Emacs. + +;;; Commentary: + +;; A cornucopia of useful interactive commands to make your Emacs +;; experience more enjoyable. + +;;; License: + +;; This program is free software; you can redistribute it and/or +;; modify it under the terms of the GNU General Public License +;; as published by the Free Software Foundation; either version 3 +;; of the License, or (at your option) any later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +;; Boston, MA 02110-1301, USA. + +;;; Code: + +(require 'thingatpt) +(require 'seq) + +(defun crux-open-with (arg) + "Open visited file in default external program. +When in dired mode, open file under the cursor. + +With a prefix ARG always prompt for command to use." + (interactive "P") + (let* ((current-file-name + (if (eq major-mode 'dired-mode) + (dired-get-file-for-visit) + buffer-file-name)) + (open (pcase system-type + (`darwin "open") + ((or `gnu `gnu/linux `gnu/kfreebsd) "xdg-open"))) + (program (if (or arg (not open)) + (read-shell-command "Open current file with: ") + open))) + (start-process "crux-open-with-process" nil program current-file-name))) + +(defun crux-buffer-mode (buffer-or-name) + "Retrieve the `major-mode' of BUFFER-OR-NAME." + (with-current-buffer buffer-or-name + major-mode)) + +(defvar crux-term-buffer-name "ansi" + "The default `ansi-term' name used by `crux-visit-term-buffer'. +This variable can be set via .dir-locals.el to provide multi-term support.") + +(defun crux-visit-term-buffer () + "Create or visit a terminal buffer." + (interactive) + (crux-start-or-switch-to (lambda () + (ansi-term crux-shell (concat crux-term-buffer-name "-term"))) + (format "*%s-term*" crux-term-buffer-name))) + +(defun crux-indent-rigidly-and-copy-to-clipboard (begin end arg) + "Indent region between BEGIN and END by ARG columns and copy to clipboard." + (interactive "r\nP") + (let ((arg (or arg 4)) + (buffer (current-buffer))) + (with-temp-buffer + (insert-buffer-substring-no-properties buffer begin end) + (indent-rigidly (point-min) (point-max) arg) + (clipboard-kill-ring-save (point-min) (point-max))))) + +(defun crux-smart-open-line-above () + "Insert an empty line above the current line. +Position the cursor at it's beginning, according to the current mode." + (interactive) + (move-beginning-of-line nil) + (newline-and-indent) + (forward-line -1) + (indent-according-to-mode)) + +(defun crux-smart-open-line (arg) + "Insert an empty line after the current line. +Position the cursor at its beginning, according to the current mode. + +With a prefix ARG open line above the current line." + (interactive "P") + (if arg + (crux-smart-open-line-above) + (progn + (move-end-of-line nil) + (newline-and-indent)))) + +(defun crux-top-join-line () + "Join the current line with the line beneath it." + (interactive) + (delete-indentation 1)) + +(defun crux-kill-whole-line (&optional arg) + "A simple wrapper around command `kill-whole-line' that respects indentation. +Passes ARG to command `kill-whole-line' when provided." + (interactive "p") + (kill-whole-line arg) + (back-to-indentation)) + +(defun crux-kill-like-backwards () + "Kill line backwards and adjust the indentation." + (interactive) + (kill-line 0) + (indent-according-to-mode)) + +(defun crux-move-beginning-of-line (arg) + "Move point back to indentation of beginning of line. + +Move point to the first non-whitespace character on this line. +If point is already there, move to the beginning of the line. +Effectively toggle between the first non-whitespace character and +the beginning of the line. + +If ARG is not nil or 1, move forward ARG - 1 lines first. If +point reaches the beginning or end of the buffer, stop there." + (interactive "^p") + (setq arg (or arg 1)) + + ;; Move lines first + (when (/= arg 1) + (let ((line-move-visual nil)) + (forward-line (1- arg)))) + + (let ((orig-point (point))) + (back-to-indentation) + (when (= orig-point (point)) + (move-beginning-of-line 1)))) + +(defun crux-indent-defun () + "Indent the current defun." + (interactive) + (save-excursion + (mark-defun) + (indent-region (region-beginning) (region-end)))) + +(defun crux-get-positions-of-line-or-region () + "Return positions (beg . end) of the current line +or region." + (let (beg end) + (if (and mark-active (> (point) (mark))) + (exchange-point-and-mark)) + (setq beg (line-beginning-position)) + (if mark-active + (exchange-point-and-mark)) + (setq end (line-end-position)) + (cons beg end))) + +(defun crux-rename-buffer-and-file () + "Rename current buffer and if the buffer is visiting a file, rename it too." + (interactive) + (let ((filename (buffer-file-name))) + (if (not (and filename (file-exists-p filename))) + (rename-buffer (read-from-minibuffer "New name: " (buffer-name))) + (let ((new-name (read-file-name "New name: " filename))) + (cond + ((vc-backend filename) (vc-rename-file filename new-name)) + (t + (rename-file filename new-name t) + (set-visited-file-name new-name t t))))))) + +(defun crux-delete-file-and-buffer () + "Kill the current buffer and deletes the file it is visiting." + (interactive) + (let ((filename (buffer-file-name))) + (when filename + (if (vc-backend filename) + (vc-delete-file filename) + (when (y-or-n-p (format "Are you sure you want to delete %s? " filename)) + (delete-file filename delete-by-moving-to-trash) + (message "Deleted file %s" filename) + (kill-buffer)))))) + +(defun crux-view-url () + "Open a new buffer containing the contents of URL." + (interactive) + (let* ((default (thing-at-point-url-at-point)) + (url (read-from-minibuffer "URL: " default))) + (switch-to-buffer (url-retrieve-synchronously url)) + (rename-buffer url t) + (goto-char (point-min)) + (re-search-forward "^$") + (delete-region (point-min) (point)) + (delete-blank-lines) + (set-auto-mode))) + +(defun crux-cleanup-buffer-or-region () + "Cleanup a region if selected, otherwise the whole buffer." + (interactive) + (call-interactively 'untabify) + (unless (member major-mode crux-indent-sensitive-modes) + (call-interactively 'indent-region)) + (whitespace-cleanup)) + +(defun crux-eval-and-replace () + "Replace the preceding sexp with its value." + (interactive) + (let ((value (eval (preceding-sexp)))) + (backward-kill-sexp) + (insert (format "%s" value)))) + +(defun crux-recompile-init () + "Byte-compile all your dotfiles again." + (interactive) + (byte-recompile-directory crux-dir 0)) + +(defun crux-file-owner-uid (filename) + "Return the UID of the FILENAME as an integer. + +See `file-attributes' for more info." + (nth 2 (file-attributes filename 'integer))) + +(defun crux-file-owned-by-user-p (filename) + "Return t if file FILENAME is owned by the currently logged in user." + (equal (crux-file-owner-uid filename) + (user-uid))) + +(defun crux-find-alternate-file-as-root (filename) + "Wraps `find-alternate-file' with opening a file as root." + (find-alternate-file (concat "/sudo:root@localhost:" filename))) + +(require 'ido) +(defun crux-sudo-edit (&optional arg) + "Edit currently visited file as root. + +With a prefix ARG prompt for a file to visit. +Will also prompt for a file to visit if current +buffer is not visiting a file." + (interactive "P") + (if (or arg (not buffer-file-name)) + (find-file (concat "/sudo:root@localhost:" + (ido-read-file-name "Find file(as root): "))) + (crux-find-alternate-file-as-root buffer-file-name))) + +(defun crux-reopen-as-root () + "Find file as root if necessary." + (unless (or (tramp-tramp-file-p buffer-file-name) + (equal major-mode 'dired-mode) + (not (file-exists-p (file-name-directory buffer-file-name))) + (file-writable-p buffer-file-name) + (crux-file-owned-by-user-p buffer-file-name)) + (crux-find-alternate-file-as-root buffer-file-name))) +(add-hook 'find-file-hook 'crux-reopen-as-root) + +(defun crux-start-or-switch-to (function buffer-name) + "Invoke FUNCTION if there is no buffer with BUFFER-NAME. +Otherwise switch to the buffer named BUFFER-NAME. Don't clobber +the current buffer." + (if (not (get-buffer buffer-name)) + (progn + (split-window-sensibly (selected-window)) + (other-window 1) + (funcall function)) + (switch-to-buffer-other-window buffer-name))) + +(defun crux-insert-date () + "Insert a timestamp according to locale's date and time format." + (interactive) + (insert (format-time-string "%c" (current-time)))) + +(defun crux-recentf-ido-find-file () + "Find a recent file using ido." + (interactive) + (let ((file (ido-completing-read "Choose recent file: " + (mapcar #'abbreviate-file-name recentf-list) + nil t))) + (when file + (find-file file)))) + +(defun crux-swap-windows () + "If you have 2 windows, it swaps them." + (interactive) + (if (/= (count-windows) 2) + (message "You need exactly 2 windows to do this.") + (let* ((w1 (car (window-list))) + (w2 (cadr (window-list))) + (b1 (window-buffer w1)) + (b2 (window-buffer w2)) + (s1 (window-start w1)) + (s2 (window-start w2))) + (set-window-buffer w1 b2) + (set-window-buffer w2 b1) + (set-window-start w1 s2) + (set-window-start w2 s1))) + (other-window 1)) + +(defun crux-switch-to-previous-buffer () + "Switch to previously open buffer. +Repeated invocations toggle between the two most recently open buffers." + (interactive) + (switch-to-buffer (other-buffer (current-buffer) 1))) + +(defun crux-kill-other-buffers () + "Kill all buffers but the current one. +Doesn't mess with special buffers." + (interactive) + (when (y-or-n-p "Are you sure you want to kill all buffers but the current one? ") + (seq-each + #'kill-buffer + (delete (current-buffer) (seq-filter #'buffer-file-name (buffer-list)))))) + +(defun crux-create-scratch-buffer () + "Create a new scratch buffer." + (interactive) + (let ((buf (generate-new-buffer "*scratch*"))) + (switch-to-buffer buf) + (funcall initial-major-mode))) + +(defun crux-find-user-init-file () + "Edit the `user-init-file', in another window." + (interactive "P") + (find-file-other-window user-init-file) + ) + +(defun crux-find-shell-init-file () + "Edit the shell init file in another window." + (interactive) + (let* ((shell (car (reverse (split-string (getenv "SHELL") "/" t)))) + (shell-init-file (cond + ((string= "zsh" shell) ".zshrc") + ((string= "bash" shell) ".bashrc") + (t (error "Unknown shell"))))) + (find-file-other-window (expand-file-name shell-init-file (getenv "HOME"))))) + +;;;###autoload +(defun crux-setup-default-keybindings () + "Set the recommended global keybindings for crux's commands." + (interactive) + (global-set-key [remap move-beginning-of-line] #'crux-move-beginning-of-line) + (global-set-key (kbd "C-c o") #'crux-open-with) + ;; mimic popular IDEs binding, note that it doesn't work in a terminal session + (global-set-key [(shift return)] #'crux-smart-open-line) + (global-set-key (kbd "M-o") #'crux-smart-open-line) + (global-set-key [(control shift return)] #'crux-smart-open-line-above) + (global-set-key (kbd "C-c n") #'crux-cleanup-buffer-or-region) + (global-set-key (kbd "C-c f") #'crux-recentf-ido-find-file) + (global-set-key (kbd "C-M-z") #'crux-indent-defun) + (global-set-key (kbd "C-c u") #'crux-view-url) + (global-set-key (kbd "C-c e") #'crux-eval-and-replace) + (global-set-key (kbd "C-c s") #'crux-swap-windows) + (global-set-key (kbd "C-c D") #'crux-delete-file-and-buffer) + (global-set-key (kbd "C-c r") #'crux-rename-buffer-and-file) + (global-set-key (kbd "C-c t") #'crux-visit-term-buffer) + (global-set-key (kbd "C-c k") #'crux-kill-other-buffers) + (global-set-key (kbd "C-c TAB") #'crux-indent-rigidly-and-copy-to-clipboard) + (global-set-key (kbd "C-c I") #'crux-find-user-init-file) + (global-set-key (kbd "C-c S") #'crux-find-shell-init-file) + (global-set-key (kbd "s-r") #'crux-recentf-ido-find-file) + (global-set-key (kbd "s-j") #'crux-top-join-line) + (global-set-key (kbd "C-^") #'crux-top-join-line) + (global-set-key (kbd "s-k") #'crux-kill-whole-line) + (global-set-key (kbd "C-<backspace>" #'crux-kill-line-backwards)) + (global-set-key (kbd "s-o") #'crux-smart-open-line-above) + (global-set-key [remap kill-whole-line] #'crux-kill-whole-line)) + +(provide 'crux) +;;; crux.el ends here