diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..467b30c --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/.lsp-session-v1 +/autosaves/ +/eln-cache/ +/elpa/ +/forge-database.sqlite +/history +/ltximg/ +/places +/projectile-bookmarks.eld +/projects +/recentf +/transient/ diff --git a/init.org b/init.org index 3db3eb7..043bd73 100644 --- a/init.org +++ b/init.org @@ -22,11 +22,15 @@ These are some packages which I might want to use in the future, or maybe not, b Completion engine alternative to helm, super lightweight - https://github.com/rougier/svg-tag-mode create svg images to replace tokens based on regexes +- https://github.com/minad/org-modern + maybe for em dashes? does loads more too. -look at org-bullets to make a package which turns =--= and =---= into \em and \en. +look at org-bullets to make a package which turns =--= and =---= into $\textemdash$ and $\textemdash$. +-> https://github.com/jorgenschaefer/typoel perchance? ** Links https://emacs.stackexchange.com/questions/12383/how-to-unbind-a-key +https://github.com/noctuid/evil-guide?tab=readme-ov-file#keymap-precedence * Start ** Lexical Scoping @@ -36,7 +40,7 @@ https://emacs.stackexchange.com/questions/12383/how-to-unbind-a-key ** Tangling init.org From Sophie's emacs.d: -#+begin_src emacs-lisp +#+begin_src emacs-lisp :tangle no (defun tangle-init () "If the current buffer is init.org the code-blocks are tangled, and the tangled file is compiled." @@ -88,6 +92,34 @@ Remove visual elements: use-dialog-box nil) #+end_src +Warnings crop up every so often and they are usually unimportant. +I disable this buffer when actively working on my config. +#+begin_src emacs-lisp :tangle no + (setq native-comp-async-report-warnings-errors 'silent) ;; native-comp warning + (setq byte-compile-warnings '(not free-vars unresolved noruntime lexical make-local)) +#+end_src + +Backups / Auto-saves +#+begin_src emacs-lisp + (defvar emacs-autosave-directory + (concat user-emacs-directory "autosaves/") + "This variable dictates where to put auto saves. It is set to a + directory called autosaves located wherever your .emacs.d/ is + located.") + + ;; Sets all files to be backed up and auto saved in a single directory. + (setq backup-directory-alist + `((".*" . ,emacs-autosave-directory)) + auto-save-file-name-transforms + `((".*" ,emacs-autosave-directory t))) + + (use-package auto-save-buffers-enhanced + :ensure t + :config + (auto-save-buffers-enhanced t) + (setq auto-save-buffers-enhanced-exclude-regexps '("init.org"))) +#+end_src + ** Package repositories #+begin_src emacs-lisp (require 'package) @@ -95,6 +127,7 @@ Remove visual elements: (require 'use-package-ensure) (setq use-package-always-ensure t) #+end_src + Add package repositories and rank them by priority #+begin_src emacs-lisp (setq package-archives @@ -114,7 +147,14 @@ Add package repositories and rank them by priority ; (package-refresh-contents) #+end_src -** Globals +#+begin_src emacs-lisp + (defvar local-lisp (concat user-emacs-directory "local-lisp/")) + (add-to-list 'load-path local-lisp) + (let ((default-directory local-lisp)) + (normal-top-level-add-subdirs-to-load-path)) +#+end_src + +** Global Variables #+begin_src emacs-lisp (set-language-environment "UTF-8") (column-number-mode t) ;; Show current column number in mode line @@ -129,6 +169,20 @@ Add package repositories and rank them by priority (show-paren-mode t) ;; Highlight matching parentheses #+end_src +#+begin_src emacs-lisp + (setq-default tab-width 4 + fill-column 80 + indent-tabs-mode nil) +#+end_src + + #+begin_src emacs-lisp + (use-package exec-path-from-shell + :ensure t + :config + (when (daemonp) + (exec-path-from-shell-initialize))) + #+end_src + Keymap: #+begin_src emacs-lisp (defvar custom-bindings-map (make-keymap) @@ -141,9 +195,9 @@ Keymap: #+end_src * Visuals ** Themes -[[https://github.com/mswift42/light-soap-theme][Light Soap Theme]] for writing +Light theme for writing #+begin_src emacs-lisp - (use-package light-soap-theme + (use-package gruvbox-theme :ensure t) #+end_src @@ -163,7 +217,7 @@ Keymap: Manage themes, use dark-theme by default: #+begin_src emacs-lisp (defvar nemo/dark-theme 'doom-one) - (defvar nemo/light-theme 'light-soap-theme) + (defvar nemo/light-theme 'gruvbox-light-medium) (load-theme nemo/dark-theme t) #+end_src @@ -317,17 +371,59 @@ Honestly not very happy with this at the moment, but it's kind of hacked togethe :inherit 'mode-line-position-face :foreground "black" :background "#eab700") #+end_src - * Packages -** magit -Magit is awesome and the number one reason why I use emacs! +** Web Search #+begin_src emacs-lisp - (use-package magit + (use-package engine-mode :defer t + :config + (defengine duckduckgo + "https://duckduckgo.com/?q=%s" + :keybinding "g") + + (defengine twitter + "https://twitter.com/search?q=%s" + :keybinding "x") + + (defengine wikipedia + "https://www.wikipedia.org/search-redirect.php?language=en&go=Go&search=%s" + :keybinding "w" + :docstring "Searchin' the wikis.") + + (defengine wiktionary + "https://www.wikipedia.org/search-redirect.php?family=wiktionary&language=en&go=Go&search=%s" + :keybinding "d") + + (defengine wolfram-alpha + "https://www.wolframalpha.com/input/?i=%s") + + (defengine youtube + "https://www.youtube.com/results?aq=f&oq=&search_query=%s" + :keybinding "y") + (engine-mode t) ) #+end_src +** =magit= -magit-todos sounds cool. +try out =diff-hl= for highlighting diffs in magit: +#+begin_src emacs-lisp :tangle no +(use-package diff-hl + :config + (diff-hl-margin-mode) + (global-diff-hl-mode)) +#+end_src + +=magit= is awesome and the number one reason why I use Emacs! +#+begin_src emacs-lisp +(use-package magit + :defer t + :config + (setq magit-mode-quit-window 'magit-restore-window-configuration + ;;magit-auto-revert-mode t + )) +#+end_src + +=magit-todos= sounds cool. #+begin_src emacs-lisp (use-package magit-todos :after magit @@ -335,7 +431,7 @@ magit-todos sounds cool. (magit-todos-mode t)) #+end_src -And magit-forge sounds cool aswell. +And forge sounds cool as well. #+begin_src emacs-lisp (use-package forge :after magit) @@ -359,17 +455,21 @@ Used by Spacemacs as well. ** evil #+begin_src emacs-lisp - (use-package evil - :ensure t - :init - (setq evil-want-C-u-scroll t) - :config - (evil-mode 1) +(use-package evil + :ensure t + :init + (setq evil-want-C-u-scroll t) + (setq evil-want-keybinding nil) + (setq evil-want-integration t) + :config + (evil-mode 1) #+end_src Evil keybinds: #+begin_src emacs-lisp + (evil-global-set-key 'normal (kbd "TAB") 'mode-line-other-buffer) (evil-define-key 'normal 'global (kbd "bb") 'consult-buffer) + (evil-define-key 'normal 'global (kbd "bd") 'kill-current-buffer) (evil-define-key 'normal 'global (kbd "ff") 'find-file) (evil-define-key 'normal 'global (kbd "wj") 'evil-window-down) (evil-define-key 'normal 'global (kbd "wk") 'evil-window-up) @@ -377,10 +477,18 @@ Evil keybinds: (evil-define-key 'normal 'global (kbd "wl") 'evil-window-right) ;; magit (evil-define-key 'normal 'global (kbd "gs") 'magit-status) - ; (evil-define-key 'normal 'global (kbd "gd") 'magit-diff) + ; (evil-define-key 'normal 'global (kbd "gd") 'magit-diff) + (evil-define-key 'normal org-mode-map (kbd "TAB") 'org-cycle) + + ;; org-mode + (evil-define-key 'normal 'org-mode-map (kbd "ih") 'org-insert-heading) + (evil-define-key 'normal 'org-mode-map (kbd "is") 'org-insert-subheading) + (evil-define-key 'normal 'org-mode-map (kbd "ii") 'org-insert-item) + (evil-define-key 'normal 'org-mode-map (kbd "ib") 'org-insert-structure-template) + (evil-define-key 'normal 'org-mode-map (kbd "il") 'org-insert-link) #+end_src -Close =config=. +Close =:config=. #+begin_src emacs-lisp ) #+end_src @@ -395,13 +503,13 @@ Close =config=. #+begin_src emacs-lisp (use-package evil-collection :after evil - :defer t + :ensure t :config (evil-collection-init)) #+end_src -** undo-fu -Also use undo-fu, which evil can use. +** =undo-fu= +Also use =undo-fu=, which evil can use. #+begin_src emacs-lisp (use-package undo-fu :defer t) @@ -416,7 +524,7 @@ Also use undo-fu, which evil can use. (projectile-mode)) #+end_src -** Ripgrep & Co. +** =ripgrep= & Co. #+begin_src emacs-lisp (use-package ripgrep :defer t) @@ -432,11 +540,16 @@ Also use undo-fu, which evil can use. #+end_src * Stuff ** Minibuffer escape -#+begin_src emacs-lisp - (setq minibuffer-prompt-properties - '(read-only t intangible t cursor-intangible t face minibuffer-prompt)) - (add-hook 'minibuffer-setup-hook #'cursor-intangible-mode) +#+begin_src emacs-lisp :tangle no +(setq minibuffer-prompt-properties + '(read-only t + intangible t + cursor-intangible t + face minibuffer-prompt)) +(add-hook 'minibuffer-setup-hook #'cursor-intangible-mode) +#+end_src +#+begin_src emacs-lisp (defun nemo/abort-minibuffer-if-active () "Abort the minibuffer if it is active." (interactive) @@ -446,6 +559,16 @@ Also use undo-fu, which evil can use. (global-set-key (kbd "") 'nemo/abort-minibuffer-if-active) #+end_src * Writing +** Olivetti +#+begin_src emacs-lisp + (use-package olivetti + :defer t + ; :bind (:map custom-bindings-map ("C-c o" . olivetti-mode)) + :config + (setq olivetti-style t)) + + (add-hook 'olivetti-mode-on-hook (lambda () (olivetti-set-width 88))) +#+end_src ** Spelling #+begin_src emacs-lisp (use-package jinx @@ -464,7 +587,7 @@ Also use undo-fu, which evil can use. (LaTeX-mode . reftex-mode) ; (LaTeX-mode . (lambda () (corfu-mode -1))) ; (LaTeX-mode . outline-minor-mode) - ; (LaTeX-mode . olivetti-mode) + (LaTeX-mode . olivetti-mode) ) #+end_src @@ -480,7 +603,7 @@ Taken from [[https://sophiebos.io/posts/prettifying-emacs-org-mode/][ here]]. #+begin_src emacs-lisp (use-package org :defer t - ; :hook (org-mode . olivetti-mode) + :hook (org-mode . olivetti-mode) :hook (org-mode . variable-pitch-mode) ; I basically always want to be running =visual-line-mode= anyway, but certainly in org-mode. :hook (org-mode . visual-line-mode) @@ -502,6 +625,11 @@ Change heading font sizes: '(outline-9 ((t (:height 1.1))))) #+end_src +Open Org files with the content folded away: +#+begin_src emacs-lisp +(setq org-startup-folded 'content) +#+end_src + Enable LaTeX previews. #+begin_src emacs-lisp (setq org-startup-with-latex-preview t) @@ -539,12 +667,20 @@ End =:config= #+end_src Use org-bullets for fancy headline markers -#+begin_src emacs-lisp +#+begin_src emacs-lisp :tangle no (use-package org-bullets :hook (org-mode . (lambda () (org-bullets-mode 1))) :config) #+end_src +Use typo-mode in org-mode for en and em dashes: +#+begin_src emacs-lisp + (require 'typo) + + (typo-global-mode 1) + (add-hook 'org-mode-hook 'typo-mode) +#+end_src + Use =org-fragtog= to show embedded LaTeX fragments when in insert mode. #+begin_src emacs-lisp (use-package org-fragtog @@ -563,6 +699,13 @@ Using =org-appear= we can hide emphasis markers for italic, bold, etc. and show org-appear-autosubmarkers t)) ;; show _.._ and superscript markers #+end_src +#+begin_src emacs-lisp + (use-package org-modern + :defer t + :after org + :hook (org-mode . org-modern-mode)) +#+end_src + Change org face fonts and font sizes for headers. #+begin_src emacs-lisp :tangle no (require 'org-faces) @@ -617,8 +760,8 @@ prettify symbols for quotes and source blocks in org-mode. (prettify-symbols-mode)) #+end_src -svg-tags-mode for fancy svg images -#+begin_src emacs-lisp :tangle no +=svg-tags-mode= for fancy SVG images +#+begin_src emacs-lisp (use-package svg-tag-mode :config (setq svg-tag-tags @@ -627,6 +770,27 @@ svg-tags-mode for fancy svg images #+end_src * Agenda +#+begin_src emacs-lisp + (require 'org) + + (setq org-agenda-start-on-weekday nil + org-agenda-block-separator nil + org-agenda-remove-tags t) + + (use-package org-super-agenda + :after org + :config + (org-super-agenda-mode)) + + (setq org-agenda-files (list "~/Shared/agenda.org" + "~/notes.org" + "~/projects.org")) + + (add-hook 'emacs-startup-hook + (lambda () (progn (org-agenda nil "a") + (delete-other-windows) + (olivetti-mode)))) +#+end_src * Navigation / Minibuffer Realistically, I will probably never use 90% of Helm's functionality, so vertico should be sufficient. ** Treemacs @@ -672,44 +836,56 @@ Realistically, I will probably never use 90% of Helm's functionality, so vertico (use-package savehist :init (savehist-mode)) #+end_src +Use consult-xref for lsp-xref and xref-find-references. #+begin_src emacs-lisp (use-package consult :bind (:map custom-bindings-map ("C-x b" . consult-buffer) - ("M-g g" . consult-goto-line))) + ("M-g g" . consult-goto-line)) + :init + (setq xref-show-xrefs-function 'consult-xref + xref-show-definitions-function 'consult-xref) + :config + ()) #+end_src #+begin_src emacs-lisp (use-package marginalia - :init + :init (marginalia-mode 1)) #+end_src -#+begin_src emacs-lisp - (use-package corfu - :custom - (corfu-auto t) ;; Enable auto completion - (corfu-cycle t) ;; Enable cycling for `corfu-next/previous' - (corfu-auto-delay 0) ;; No delay - (corfu-auto-prefix 2) ;; Start when this many characters have been typed - (corfu-popupinfo-delay 0.5) ;; Short delay - (corfu-preselect 'prompt) ;; Preselect the prompt - :init - (global-corfu-mode)) +#+begin_src emacs-lisp :tangle no +(use-package corfu + :custom + ;; Enable auto completion + (corfu-auto t) + ;; Enable cycling for `corfu-next/previous' + (corfu-cycle t) + ;; No delay + (corfu-auto-delay 0) + ;; Start when this many characters have been typed + (corfu-auto-prefix 2) + ;; Short delay + (corfu-popupinfo-delay 0.5) + ;; Preselect the prompt + (corfu-preselect 'prompt) + :init + (global-corfu-mode)) - (use-package emacs - :init - ;; TAB cycle if there are only few candidates - (setq completion-cycle-threshold 3) +(use-package emacs + :init + ;; TAB cycle if there are only few candidates + (setq completion-cycle-threshold 3) - ;; Hide commands in M-x which do not apply to the current mode. - ;; Corfu commands are hidden, since they are not supposed to be used via M-x. - (setq read-extended-command-predicate - #'command-completion-default-include-p) + ;; Hide commands in M-x which do not apply to the current mode. Corfu + ;; commands are hidden, since they are not supposed to be used via M-x. + (setq read-extended-command-predicate + #'command-completion-default-include-p) - ;; Enable indentation+completion using the TAB key. - ;; `completion-at-point' is often bound to M-TAB. - (setq tab-always-indent 'complete)) + ;; Enable indentation+completion using the TAB key. + ;; `completion-at-point' is often bound to M-TAB. + (setq tab-always-indent 'complete)) #+end_src This package seems to slow down search quite a bit in common buffers like find-file and exectue-extended-comand: @@ -786,7 +962,7 @@ This package seems to slow down search quite a bit in common buffers like find-f #+begin_src emacs-lisp (use-package smartparens :defer t - :hook (prog-mode text-mode markdown-mode) + :hook ((prog-mode text-mode markdown-mode) . smartparens-mode) :config (require 'smartparens-config)) #+end_src @@ -794,7 +970,11 @@ This package seems to slow down search quite a bit in common buffers like find-f #+begin_src emacs-lisp (use-package company :defer t - :hook (after-init . company-mode)) + :hook (after-init . global-company-mode) + :config + (setq + company-minimum-prefix-length 1 + company-idle-delay 0.0)) #+end_src ** Flycheck #+begin_src emacs-lisp @@ -803,15 +983,44 @@ This package seems to slow down search quite a bit in common buffers like find-f :hook (after-init. global-flycheck-mode) :config) #+end_src +** Yasnippet +Auto-completion requires yasnippet for some competions, such as function arguments and parens. +#+begin_src emacs-lisp + (use-package yasnippet + :defer t + :hook ((prog-mode text-mode) . yas-minor-mode)) + + (use-package yasnippet-snippets + :ensure t) +#+end_src + ** LSP Use lsp-mode and lsp-ui for LSP functionality. Emacs has its own internal LSP client called eglot, but I've never used it and I'm relatively happy with lsp-mode. +LSP sets it's prefix key to =s-l= by default, which uses the Super key which I use as my Mod key in sway, so I can't use it in emacs. #+begin_src emacs-lisp (use-package lsp-mode :defer t :hook (prog-mode . lsp) :hook (lsp-mode . lsp-enable-which-key-integration) - :commands lsp) + :hook (lsp-mode . lsp-inlay-hints-mode) + :init + (setq lsp-keymap-prefix "C-l") + :commands lsp + :config + (setq + lsp-idle-delay 0.6 + lsp-inlay-hint-enable t + lsp-modeline-code-actions-enable t + lsp-modeline-code-actions-segments '(name count) + lsp-modeline-diagnostics-enable t + + lsp-rust-server 'rust-analyzer + lsp-rust-analyzer-server-display-inlay-hints t + lsp-rust-analyzer-proc-macro-enable t + lsp-rust-analyzer-binding-mode-hints t + lsp-rust-analyzer-display-closure-return-type-hints t + lsp-rust-analyzer-server-format-inlay-hints t)) (use-package lsp-treemacs :commands lsp-treemacs-errors-list) @@ -819,26 +1028,17 @@ Emacs has its own internal LSP client called eglot, but I've never used it and I (use-package lsp-ui :config (setq - ;;lsp-modeline-code-actions-enable nil - ;;lsp-rust-analyzer-cargo-all-targets nil - lsp-rust-server 'rust-analyzer - ;; lsp-rust-analyzer-server-command "xwin-env rust-analyzer" - lsp-ui-sideline-show-code-actions t - lsp-ui-doc-enable t - lsp-ui-doc-delay 0.5 - lsp-ui-doc-show-with-cursor t + ;;lsp-modeline-code-actions-enable nil + ;;lsp-rust-analyzer-cargo-all-targets nil + ;; lsp-rust-analyzer-server-command "xwin-env rust-analyzer" + lsp-ui-sideline-show-code-actions t + lsp-ui-doc-enable t + lsp-ui-doc-delay 0.5 + lsp-ui-doc-show-with-cursor t ; lsp-ui-doc-use-childframe t ; lsp-ui-doc-use-webkit t - lsp-inlay-hint-enable t - lsp-modeline-code-actions-enable t - lsp-modeline-code-actions-segments '(name count) - lsp-modeline-diagnostics-enable t - lsp-rust-analyzer-proc-macro-enable t - lsp-rust-analyzer-binding-mode-hints t - lsp-rust-analyzer-display-closure-return-type-hints t - lsp-rust-analyzer-server-format-inlay-hints t) - :commands lsp-ui-mode - ) + ) + :commands lsp-ui-mode) #+end_src ** Rust @@ -852,12 +1052,61 @@ Emacs has its own internal LSP client called eglot, but I've never used it and I :config (setq rust-format-on-save t)) #+end_src + +#+begin_src emacs-lisp + (use-package rust-playground + :defer t) +#+end_src + [[https://github.com/kwrooijen/cargo.el][cargo-mode]] #+begin_src emacs-lisp (use-package cargo-mode :defer t :hook (rust-mode . cargo-minor-mode)) #+end_src + +** Web +#+begin_src emacs-lisp + (use-package web-mode + :defer t + :mode + (("\\.phtml\\'" . web-mode) + ("\\.tpl\\.php\\'" . web-mode) + ("\\.twig\\'" . web-mode) + ("\\.xml\\'" . web-mode) + ("\\.html\\'" . web-mode) + ("\\.htm\\'" . web-mode) + ("\\.[gj]sp\\'" . web-mode) + ("\\.as[cp]x?\\'" . web-mode) + ("\\.eex\\'" . web-mode) + ("\\.erb\\'" . web-mode) + ("\\.mustache\\'" . web-mode) + ("\\.handlebars\\'" . web-mode) + ("\\.hbs\\'" . web-mode) + ("\\.eco\\'" . web-mode) + ("\\.ejs\\'" . web-mode) + ("\\.svelte\\'" . web-mode) + ("\\.ctp\\'" . web-mode) + ("\\.djhtml\\'" . web-mode)) + :config + (setq + web-mode-css-indent-offset 2 + web-mode-code-indent-offset 2 + web-mode-attr-indent-offset 2 + web-mode-markup-indent-offset 2)) +#+end_src + +** C/C++ +clang-format +#+begin_src emacs-lisp + (use-package clang-format + :defer t) +#+end_src + * Keybinds #+begin_src emacs-lisp + (define-key custom-bindings-map (kbd "C-c l") 'org-store-link) + (define-key custom-bindings-map (kbd "C-c a") 'org-agenda) + (define-key custom-bindings-map (kbd "C-c c") 'org-capture) + (define-key custom-bindings-map (kbd "C-c t") 'org-todo) #+end_src diff --git a/local-lisp/typo.el b/local-lisp/typo.el new file mode 100644 index 0000000..d8bd42b --- /dev/null +++ b/local-lisp/typo.el @@ -0,0 +1,471 @@ +;;; typo.el --- Minor mode for typographic editing + +;; Copyright (C) 2012 Jorgen Schaefer + +;; Version: 1.1 +;; Author: Jorgen Schaefer +;; URL: https://github.com/jorgenschaefer/typoel +;; Created: 6 Feb 2012 +;; Keywords: convenience, wp + +;; 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 this program; if not, write to the Free Software +;; Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +;; 02110-1301 USA + +;;; Commentary: + +;; typo.el includes two modes, `typo-mode` and `typo-global-mode`. +;; +;; `typo-mode` is a buffer-specific minor mode that will change a number +;; of normal keys to make them insert typographically useful unicode +;; characters. Some of those keys can be used repeatedly to cycle through +;; variations. This includes in particular quotation marks and dashes. +;; +;; `typo-global-mode` introduces a global minor mode which adds the +;; `C-c 8` prefix to complement Emacs’ default `C-x 8` prefix map. +;; +;; See the documentation of `typo-mode` and `typo-global-mode` for +;; further details. +;; +;; ## Quotation Marks +;; +;; > “He said, ‘leave me alone,’ and closed the door.” +;; +;; All quotation marks in this sentence were added by hitting the " key +;; exactly once each. typo.el guessed the correct glyphs to use from +;; context. If it gets it wrong, you can just repeat hitting the " key +;; until you get the quotation mark you wanted. +;; +;; `M-x typo-change-language` lets you change which quotation marks to +;; use. This is also configurable, in case you want to add your own. +;; +;; ## Dashes and Dots +;; +;; The hyphen key will insert a default hyphen-minus glyph. On repeated +;; use, though, it will cycle through the en-dash, em-dash, and a number +;; of other dash-like glyphs available in Unicode. This means that typing +;; two dashes inserts an en-dash and typing three dashes inserts an +;; em-dash, as would be expected. The name of the currently inserted dash +;; is shown in the minibuffer. +;; +;; The full stop key will self-insert as usual. When three dots are +;; inserted in a row, though, they are replaced by a horizontal ellipsis +;; glyph. +;; +;; ## Other Keys +;; +;; Tick and backtick keys insert the appropriate quotation mark as well. +;; The less-than and greater-than signs cycle insert the default glyphs +;; on first use, but cycle through double and single guillemets on +;; repeated use. +;; +;; ## Prefix Map +;; +;; In addition to the above, typo-global-mode also provides a +;; globally-accessible key map under the `C-c 8` prefix (akin to Emacs’ +;; default `C-x 8` prefix map) to insert various Unicode characters. +;; +;; In particular, `C-c 8 SPC` will insert a no-break space. Continued use +;; of SPC after this will cycle through half a dozen different space +;; types available in Unicode. +;; +;; Check the mode’s documentation for more details. + +;;; Code: + +;; For some reason, Emacs default has these as parentheses. This is +;; completely confusing when mixing this with normal parentheses, +;; and gets e.g. the following code wrong, even. Punctuation syntax +;; results in much more intuitive behavior. +(modify-syntax-entry ?» ".") +(modify-syntax-entry ?« ".") +;; Sorry for the intrusion. + +(defgroup typo nil + "*Typography mode for Emacs" + :prefix "typo-" + :group 'convenience) + +(defcustom typo-quotation-marks + '(("Czech" "„" "“" "‚" "‘") + ("Czech (Guillemets)" "»" "«" "›" "‹") + ("English" "“" "”" "‘" "’") + ("German" "„" "“" "‚" "‘") + ("German (Guillemets)" "»" "«" "›" "‹") + ("French" "«" "»" "‹" "›") + ("Finnish" "”" "”" "’" "’") + ("Finnish (Guillemets)" "»" "»" "›" "›") + ("Swedish" "”" "”" "’" "’") + ("Russian" "«" "»" "„" "“") + ("Italian" "«" "»" "“" "”") + ("Polish" "„" "”" "‚" "’") + ("Serbian" "„" "”" "’" "’") + ("Ukrainian" "«" "»" "„" "“")) + "*Quotation marks per language." + :type '(repeat (list (string :tag "Language") + (string :tag "Double Opening Quotation Mark") + (string :tag "Double Closing Quotation Mark") + (string :tag "Single Opening Quotation Mark") + (string :tag "Single Closing Quotation Mark"))) + :group 'typo) + + +(defcustom typo-language "English" + "*The default language typo-mode should use." + :type '(string :tag "Default Language") + :group 'typo) +(make-variable-buffer-local 'typo-language) +(put 'typo-language 'safe-local-variable 'stringp) + +(defcustom typo-disable-electricity-functions '(typo-in-xml-tag) + "*A list of functions to call before an electric key binding is +used. If one of the functions returns non-nil, the key +self-inserts. + +This can be used to disable the electric keys in e.g. XML tags." + :type 'hook + :options '(typo-in-xml-tag) + :group 'typo) + +(defvar typo-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "\"") 'typo-insert-quotation-mark) + (define-key map (kbd "'") 'typo-cycle-right-single-quotation-mark) + (define-key map (kbd "`") 'typo-cycle-left-single-quotation-mark) + (define-key map (kbd "-") 'typo-cycle-dashes) + (define-key map (kbd ".") 'typo-cycle-ellipsis) + (define-key map (kbd "<") 'typo-cycle-left-angle-brackets) + (define-key map (kbd ">") 'typo-cycle-right-angle-brackets) + map) + "The keymap for `typo-mode'.") + +(defvar typo-global-mode-map + (let ((gmap (make-sparse-keymap)) + (map (make-sparse-keymap))) + (define-key gmap (kbd "C-c 8") map) + (define-key map (kbd "\"") 'typo-insert-quotation-mark) + (define-key map (kbd "'") 'typo-cycle-right-single-quotation-mark) + (define-key map (kbd "`") 'typo-cycle-left-single-quotation-mark) + (define-key map (kbd "--") 'typo-cycle-dashes) + (define-key map (kbd ".") 'typo-cycle-ellipsis) + (define-key map (kbd "<<") 'typo-cycle-left-angle-brackets) + (define-key map (kbd ">>") 'typo-cycle-right-angle-brackets) + (define-key map (kbd "*") 'typo-cycle-multiplication-signs) + (define-key map (kbd "SPC") 'typo-cycle-spaces) + (define-key map (kbd "?") 'typo-cycle-question-mark) + (define-key map (kbd "!") 'typo-cycle-exclamation-mark) + (define-key map (kbd "/=") "≠") + (define-key map (kbd "//") "÷") + (define-key map (kbd ">=") "≥") + (define-key map (kbd "<=") "≤") + (define-key map (kbd "=<") "⇐") + (define-key map (kbd "=>") "⇒") + (define-key map (kbd "<-") "←") + (define-key map (kbd "-<") "←") + (define-key map (kbd "->") "→") + (define-key map (kbd "-^") "↑") + (define-key map (kbd "=^") "⇑") + (define-key map (kbd "-v") "↓") + (define-key map (kbd "=v") "⇓") + (define-key map (kbd "T") "™") + gmap) + "The keymap for `typo-global-mode'.") + +;;;###autoload +(define-minor-mode typo-mode + "Minor mode for typographic editing. + +This mode changes some default keybindings to enter typographic +glyphs. In particular, this changes how quotation marks, the +dash, the dot, and the angle brackets work. + +Most keys will cycle through various options when used +repeatedly. + +\\{typo-mode-map}" + :group 'typo + :lighter " Typo" + :keymap typo-mode-map) + +;;;###autoload +(define-minor-mode typo-global-mode + "Minor mode for typographic editing. + +This mode provides a prefix map under C-c 8 which complements the +default C-x 8 prefix map. + +\\{typo-global-mode-map}" + :group 'typo + :global t + :keymap typo-global-mode-map) + +(defun typo-change-language (language) + "Change the current language used for quotation marks." + (interactive (list (completing-read + "Quotation marks: " + typo-quotation-marks + ))) + (when (not (assoc-string language typo-quotation-marks)) + (error "Unknown language %s (see `typo-quotation-marks')" language)) + (setq typo-language language)) + +(defun typo-open-double-quotation-mark () + "Return the opening double quotation marks for the current language." + (nth 1 (assoc-string typo-language typo-quotation-marks))) + +(defun typo-close-double-quotation-mark () + "Return the closing double quotation marks for the current language." + (nth 2 (assoc-string typo-language typo-quotation-marks))) + +(defun typo-open-single-quotation-mark () + "Return the opening single quotation marks for the current language." + (nth 3 (assoc-string typo-language typo-quotation-marks))) + +(defun typo-close-single-quotation-mark () + "Return the closing single quotation marks for the current language." + (nth 4 (assoc-string typo-language typo-quotation-marks))) + +(defun typo-in-xml-tag () + "Return non-nil if point is inside an XML tag." + (save-excursion + (and (re-search-backward "[<>]" + ;; If you have an XML tag that spans more + ;; than 25 lines, you should be shot. + (max (point-min) + (- (point) + (* 80 25))) + t) + ;; < without a word char is a math formula + (looking-at "<\\w")))) + +(defun typo-electricity-disabled-p () + "Return non-nil if electricity is disabled at point. + +See `typo-disable-electricity-functions'." + ;; Only if this happened from a non-prefix variable + (and (= (length (this-single-command-keys)) 1) + (run-hook-with-args-until-success 'typo-disable-electricity-functions))) + +(defun typo-quotation-needs-closing (open close) + "Return non-nil if the last occurrence of either OPEN and CLOSE +in the current buffer is OPEN, i.e. if this pair still needs +closing. + +This does not support nested, equal quotation marks." + (save-excursion + (if (re-search-backward (regexp-opt (list open close)) + nil t) + (equal open (match-string 0)) + nil))) + +(defun typo-insert-quotation-mark (arg) + "Insert quotation marks. + +This command tries to be intelligent. Opening quotation marks are +closed. If you repeat the command after a quotation mark, that +mark is cycled through various variants. + +After a closing double quotation mark, the next variant is an +opening single quotation mark. So when this command is issued +inside a quotation, it will first close the quotation. On the +second time, it will open an inner quotation. + +After an opening double quotation mark, the next variant is the +typewriter quotation mark, making it possible in the usual case +to simple issue this command twice to get a typewriter quotation +mark (use C-q \" or C-o \" to force inserting one). + +If used with a numeric prefix argument, only typewriter quotation +marks will be inserted." + (interactive "P") + (if (or (typo-electricity-disabled-p) arg) + (call-interactively 'self-insert-command) + (let* ((double-open (typo-open-double-quotation-mark)) + (double-close (typo-close-double-quotation-mark)) + (double-needs-closing (typo-quotation-needs-closing + double-open double-close)) + + (single-open (typo-open-single-quotation-mark)) + (single-close (typo-close-single-quotation-mark)) + (single-needs-closing (typo-quotation-needs-closing + single-open single-close)) + + (after-any-opening (looking-back (regexp-opt (list double-open + single-open))))) + (cond + ;; For languages that use the same symbol for opening and + ;; closing (Finnish, Swedish...), the simplest thing to do is to + ;; not try to be too smart and just cycle ” and " + ((equal double-open double-close) + (typo-insert-cycle (list double-open "\""))) + ;; Inside a single quotation, if we're not directly at the + ;; opening one, we close it. + ((and single-needs-closing + (not after-any-opening)) + (insert single-close)) + ;; Inside a double quotation, if we're not directly at the + ;; opening one ... + ((and double-needs-closing + (not after-any-opening)) + ;; ... if we are after a space, we open an inner quotation. + ;; + ;; (This misses the situation where we start a quotation with an + ;; inner quotation, but that's indistinguishable from cycling + ;; through keys, and the latter is more common.) + (if (looking-back "\\s-") + (insert single-open) + ;; Otherwise, close the double one + (insert double-close))) + ;; Nothing is open, or we are directly at an opening quote. If + ;; this is a repetition of a this command, start cycling. + ((eq this-command last-command) + (delete-char -1) + (typo-insert-cycle (list "\"" + double-open double-close + single-open single-close))) + ;; Otherwise, just open a double quotation mark. + ;; + ;; This can actually happen if we open a quotation, then move + ;; point, then go back to directly after the quotation, and then + ;; call this again. Opening another double quotation there is + ;; weird, but I'm not sure what else to do then, either. + (t + (insert double-open)))))) + +(defun typo-cycle-ellipsis (arg) + "Add periods. The third period will add an ellipsis. + +If used with a numeric prefix argument N, N periods will be inserted." + (interactive "P") + (if (or (typo-electricity-disabled-p) arg) + (call-interactively 'self-insert-command) + (if (looking-back "\\.\\.") + (replace-match "…") + (call-interactively 'self-insert-command)))) + +(defmacro define-typo-cycle (name docstring cycle) + "Define a typo command that cycles through various options. + +If used with a numeric prefix argument N, N standard characters will be +inserted instead of cycling. + +NAME is the name of the command to define. +DOCSTRING is the docstring for that command. + +CYCLE is a list of strings to cycle through." + (declare (indent 1) (doc-string 2)) + `(defun ,name (arg) + ,docstring + (interactive "P") + (if (or (typo-electricity-disabled-p) arg) + (call-interactively 'self-insert-command) + (typo-insert-cycle ',cycle)))) + +;; This event cycling loop is from `kmacro-call-macro' +(defun typo-insert-cycle (cycle) + "Insert the strings in CYCLE" + (let ((i 0) + (repeat-key last-input-event) + repeat-key-str) + (insert (nth i cycle)) + (setq repeat-key-str (format-kbd-macro (vector repeat-key) nil)) + (while repeat-key + (message "(Inserted %s; type %s for other options)" + (typo-char-name (nth i cycle)) + repeat-key-str) + (if (equal repeat-key (read-event)) + (progn + (clear-this-command-keys t) + (delete-char (- (length (nth i cycle)))) + (setq i (% (+ i 1) + (length cycle))) + (insert (nth i cycle)) + (setq last-input-event nil)) + (setq repeat-key nil))) + (when last-input-event + (clear-this-command-keys t) + (setq unread-command-events (list last-input-event))))) + +(defun typo-char-name (string) + "Return the Unicode name of the first char in STRING." + (let ((char-code (elt string 0)) + name) + (setq name (get-char-code-property char-code 'name)) + (when (or (not name) + (= ?< (elt name 0))) + (setq name (get-char-code-property char-code 'old-name))) + name)) + +(define-typo-cycle typo-cycle-right-single-quotation-mark + "Cycle through the right quotation mark and the typewriter apostrophe. + +If used with a numeric prefix argument N, N typewriter apostrophes +will be inserted." + ("’" "'")) + +(define-typo-cycle typo-cycle-left-single-quotation-mark + "Cycle through the left single quotation mark and the backtick. + +If used with a numeric prefix argument N, N backticks will be inserted." + ("‘" "`")) + +(define-typo-cycle typo-cycle-dashes + "Cycle through various dashes." + ("-" ; HYPHEN-MINUS + "–" ; EN DASH + "—" ; EM DASH + "−" ; MINUS SIGN + "‐" ; HYPHEN + "‑" ; NON-BREAKING HYPHEN + )) + +(define-typo-cycle typo-cycle-left-angle-brackets + "Cycle through the less-than sign and guillemet quotation marks. + +If used with a numeric prefix argument N, N less-than signs will be inserted." + ("<" "«" "‹")) + +(define-typo-cycle typo-cycle-right-angle-brackets + "Cycle through the greater-than sign and guillemet quotation marks. + +If used with a numeric prefix argument N, N greater-than signs will be inserted." + (">" "»" "›")) + +(define-typo-cycle typo-cycle-multiplication-signs + "Cycle through the asterisk and various multiplication signs" + ("×" "·")) + +(define-typo-cycle typo-cycle-spaces + "Cycle through various spaces" + (" " ; NO-BREAK SPACE + " " ; THIN SPACE + "‌" ; ZERO WIDTH NON-JOINER + "‍" ; ZERO WIDTH JOINER + " " ; MEDIUM MATHEMATICAL SPACE + " " ; HAIR SPACE + ;; " " ; EM SPACE + ;; " " ; EN SPACE + " " ; SPACE + )) + +(define-typo-cycle typo-cycle-question-mark + "Cycle through various interrogatory marks." + ("?" "¿" "‽" "⸘" "⸮")) + +(define-typo-cycle typo-cycle-exclamation-mark + "Cycle through various exclamatory marks." + ("!" "¡" "‽" "⸘")) + +(provide 'typo) +;;; typo.el ends here