9da3ffee41
This is a massive diff that I had to do in a hurry - when leaving Urbint. I'm pretty sure that most of these are updating Emacs packages, but I'm not positive.
2122 lines
88 KiB
EmacsLisp
2122 lines
88 KiB
EmacsLisp
;;; emojify.el --- Display emojis in Emacs -*- lexical-binding: t; -*-
|
||
|
||
;; Copyright (C) 2015-2018 Iqbal Ansari
|
||
|
||
;; Author: Iqbal Ansari <iqbalansari02@yahoo.com>
|
||
;; Keywords: multimedia, convenience
|
||
;; URL: https://github.com/iqbalansari/emacs-emojify
|
||
;; Version: 1.0
|
||
;; Package-Requires: ((seq "1.11") (ht "2.0") (emacs "24.3"))
|
||
|
||
;; 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, see <http://www.gnu.org/licenses/>.
|
||
|
||
;;; Commentary:
|
||
|
||
;; This package displays emojis in Emacs similar to how Github, Slack etc do. It
|
||
;; can display plain ascii like ':)' as well as Github style emojis like ':smile:'
|
||
;;
|
||
;; It provides a minor mode `emojify-mode' to enable display of emojis in a buffer.
|
||
;; To enable emojify mode globally use `global-emojify-mode'
|
||
;;
|
||
;; For detailed documentation see the projects README file at
|
||
;; https://github.com/iqbalansari/emacs-emojify
|
||
|
||
|
||
|
||
;;; Code:
|
||
|
||
(require 'seq)
|
||
(require 'ht)
|
||
|
||
(require 'subr-x nil :no-error)
|
||
(require 'cl-lib)
|
||
(require 'json)
|
||
(require 'regexp-opt)
|
||
(require 'jit-lock)
|
||
(require 'pcase)
|
||
(require 'tar-mode)
|
||
(require 'apropos)
|
||
|
||
|
||
|
||
;; Satisfying the byte-compiler
|
||
;; We do not "require" these functions but if `org-mode' is active we use them
|
||
|
||
;; Required to determine point is in an org-list
|
||
(declare-function org-list-get-item-begin "org-list")
|
||
(declare-function org-at-heading-p "org")
|
||
|
||
;; Required to determine point is in an org-src block
|
||
(declare-function org-element-type "org-element")
|
||
(declare-function org-element-at-point "org-element")
|
||
|
||
;; Required for integration with company-mode
|
||
(declare-function company-pseudo-tooltip-unhide "company")
|
||
|
||
;; Shouldn't require 'jit-lock be enough :/
|
||
(defvar jit-lock-start)
|
||
(defvar jit-lock-end)
|
||
|
||
;; Used while inserting emojis using helm
|
||
(defvar helm-buffer)
|
||
(defvar helm-after-initialize-hook)
|
||
|
||
|
||
|
||
;; Compatibility functions
|
||
|
||
(defun emojify-user-error (format &rest args)
|
||
"Signal a pilot error, making a message by passing FORMAT and ARGS to ‘format-message’."
|
||
(if (fboundp 'user-error)
|
||
(apply #'user-error format args)
|
||
(apply #'error format args)))
|
||
|
||
(defun emojify-face-height (face)
|
||
"Get font height for the FACE."
|
||
(let ((face-font (face-font face)))
|
||
(cond
|
||
((and (display-multi-font-p)
|
||
;; Avoid calling font-info if the frame's default font was
|
||
;; not changed since the frame was created. That's because
|
||
;; font-info is expensive for some fonts, see bug #14838.
|
||
(not (string= (frame-parameter nil 'font) face-font)))
|
||
(aref (font-info face-font) 3))
|
||
(t (frame-char-height)))))
|
||
|
||
(defun emojify-default-font-height ()
|
||
"Return the height in pixels of the current buffer's default face font.
|
||
|
||
`default-font-height' seems to be available only on Emacs versions after 24.3.
|
||
This provides a compatibility version for previous versions."
|
||
(if (fboundp 'default-font-height)
|
||
(default-font-height)
|
||
(emojify-face-height 'default)))
|
||
|
||
(defun emojify-overlays-at (pos &optional sorted)
|
||
"Return a list of the overlays that contain the character at POS.
|
||
If SORTED is non-nil, then sort them by decreasing priority.
|
||
|
||
The SORTED argument was introduced in Emacs 24.4, along with the incompatible
|
||
change that overlay priorities can be any Lisp object (earlier they were
|
||
restricted to integer and nil). This version uses the SORTED argument of
|
||
`overlays-at' on Emacs version 24.4 onwards and manually sorts the overlays by
|
||
priority on lower versions."
|
||
(if (version< emacs-version "24.4")
|
||
(let ((overlays-at-pos (overlays-at pos)))
|
||
(if sorted
|
||
(seq-sort (lambda (overlay1 overlay2)
|
||
(if (and (overlay-get overlay2 'priority)
|
||
(overlay-get overlay1 'priority))
|
||
;; If both overlays have priorities compare them
|
||
(< (overlay-get overlay1 'priority)
|
||
(overlay-get overlay2 'priority))
|
||
;; Otherwise overlay with nil priority is sorted below
|
||
;; the one with integer value otherwise preserve order
|
||
(not (overlay-get overlay1 'priority))))
|
||
overlays-at-pos)
|
||
overlays-at-pos))
|
||
(overlays-at pos sorted)))
|
||
|
||
(defun emojify--string-join (strings &optional separator)
|
||
"Join all STRINGS using SEPARATOR.
|
||
|
||
This function is available on Emacs v24.4 and higher, it has been
|
||
backported here for compatibility with older Emacsen."
|
||
(if (fboundp 'string-join)
|
||
(apply #'string-join (list strings separator))
|
||
(mapconcat 'identity strings separator)))
|
||
|
||
|
||
|
||
;; Debugging helpers
|
||
|
||
(define-minor-mode emojify-debug-mode
|
||
"Enable debugging for emojify-mode.
|
||
|
||
By default emojify silences any errors during emoji redisplay. This is done
|
||
since emojis are redisplayed using jit-lock (the same mechanism used for
|
||
font-lock) as such any bugs in the code can cause other important things to
|
||
fail. This also turns on jit-debug-mode so that (e)debugging emojify's redisplay
|
||
functions work."
|
||
:init-value nil
|
||
(if emojify-debug-mode
|
||
(when (fboundp 'jit-lock-debug-mode)
|
||
(jit-lock-debug-mode +1))
|
||
(when (fboundp 'jit-lock-debug-mode)
|
||
(jit-lock-debug-mode -1))))
|
||
|
||
(defmacro emojify-execute-ignoring-errors-unless-debug (&rest forms)
|
||
"Execute FORMS ignoring errors unless variable `emojify-debug-mode' is non-nil."
|
||
(declare (debug t) (indent 0))
|
||
`(if emojify-debug-mode
|
||
(progn
|
||
,@forms)
|
||
(ignore-errors
|
||
,@forms)))
|
||
|
||
|
||
|
||
;; Utility functions
|
||
|
||
;; These should be bound dynamically by functions calling
|
||
;; `emojify--inside-rectangle-selection-p' and
|
||
;; `emojify--inside-non-rectangle-selection-p' to region-beginning and
|
||
;; region-end respectively. This is needed mark the original region which is
|
||
;; impossible to get after point moves during processing.
|
||
(defvar emojify-region-beg nil)
|
||
(defvar emojify-region-end nil)
|
||
|
||
;; This should be bound dynamically to the location of point before emojify's
|
||
;; display loop, this since getting the point after point moves during
|
||
;; processing is impossible
|
||
(defvar emojify-current-point nil)
|
||
|
||
(defmacro emojify-with-saved-buffer-state (&rest forms)
|
||
"Execute FORMS saving current buffer state.
|
||
|
||
This saves point and mark, `match-data' and buffer modification state it also
|
||
inhibits buffer change, point motion hooks."
|
||
(declare (debug t) (indent 0))
|
||
`(let ((inhibit-point-motion-hooks t)
|
||
(emojify-current-point (point))
|
||
(emojify-region-beg (when (region-active-p) (region-beginning)))
|
||
(emojify-region-end (when (region-active-p) (region-end))))
|
||
(with-silent-modifications
|
||
(save-match-data
|
||
(save-excursion
|
||
(save-restriction
|
||
(widen)
|
||
,@forms))))))
|
||
|
||
(defmacro emojify-do-for-emojis-in-region (beg end &rest forms)
|
||
"For all emojis between BEG and END, execute the given FORMS.
|
||
|
||
During the execution `emoji-start' and `emoji-end' are bound to the start
|
||
and end of the emoji for which the form is being executed."
|
||
(declare (debug t) (indent 2))
|
||
`(let ((--emojify-loop-current-pos ,beg)
|
||
(--emojify-loop-end ,end)
|
||
emoji-start)
|
||
(while (and (> --emojify-loop-end --emojify-loop-current-pos)
|
||
(setq emoji-start (text-property-any --emojify-loop-current-pos --emojify-loop-end 'emojified t)))
|
||
(let ((emoji-end (+ emoji-start
|
||
(length (get-text-property emoji-start 'emojify-text)))))
|
||
,@forms
|
||
(setq --emojify-loop-current-pos emoji-end)))))
|
||
|
||
(defun emojify-message (format-string &rest args)
|
||
"Log debugging messages to buffer named 'emojify-log'.
|
||
|
||
This is a substitute to `message' since using it during redisplay causes errors.
|
||
FORMAT-STRING and ARGS are same as the arguments to `message'."
|
||
(when emojify-debug-mode
|
||
(emojify-with-saved-buffer-state
|
||
(with-current-buffer (get-buffer-create "emojify-log")
|
||
(goto-char (point-max))
|
||
(insert (apply #'format format-string args))
|
||
(insert "\n")))))
|
||
|
||
(defun emojify--get-relevant-region ()
|
||
"Try getting region in buffer that completely covers the current window.
|
||
|
||
This is used instead of directly using `window-start' and `window-end', since
|
||
they return the values corresponding buffer in currently selected window, which
|
||
is incorrect if the buffer where there are called is not actually the buffer
|
||
visible in the selected window."
|
||
(let* ((window-size (- (window-end) (window-start)))
|
||
(start (max (- (point) window-size) (point-min)))
|
||
(end (min (+ (point) window-size) (point-max))))
|
||
(cons start end)))
|
||
|
||
(defun emojify-quit-buffer ()
|
||
"Hide the current buffer.
|
||
There are windows other than the one the current buffer is displayed in quit the
|
||
current window too."
|
||
(interactive)
|
||
(if (= (length (window-list)) 1)
|
||
(bury-buffer)
|
||
(quit-window)))
|
||
|
||
(defvar emojify-common-mode-map
|
||
(let ((map (make-sparse-keymap)))
|
||
(define-key map "q" #'emojify-quit-buffer)
|
||
(define-key map "n" #'next-line)
|
||
(define-key map "p" #'previous-line)
|
||
(define-key map "r" #'isearch-backward)
|
||
(define-key map "s" #'isearch-forward)
|
||
(define-key map ">" #'end-of-buffer)
|
||
(define-key map "<" #'beginning-of-buffer)
|
||
|
||
(dolist (key '("?" "h" "H"))
|
||
(define-key map key #'describe-mode))
|
||
|
||
(dolist (number (number-sequence 0 9))
|
||
(define-key map (number-to-string number) #'digit-argument))
|
||
|
||
map)
|
||
"Common keybindings available in all special emojify buffers.")
|
||
|
||
|
||
|
||
;; Customizations for control how emojis are displayed
|
||
|
||
(defgroup emojify nil
|
||
"Customization options for emojify"
|
||
:group 'display
|
||
:prefix "emojify-")
|
||
|
||
(defcustom emojify-emoji-json
|
||
(expand-file-name "data/emoji.json"
|
||
(cond (load-file-name (file-name-directory load-file-name))
|
||
((locate-library "emojify") (file-name-directory (locate-library "emojify")))
|
||
(t default-directory)))
|
||
"The path to JSON file containing the configuration for displaying emojis."
|
||
:type 'file
|
||
:group 'emojify)
|
||
|
||
(defvar emojify-emoji-set-json
|
||
(let ((json-array-type 'list)
|
||
(json-object-type 'hash-table))
|
||
(json-read-file (expand-file-name "data/emoji-sets.json"
|
||
(cond (load-file-name (file-name-directory load-file-name))
|
||
((locate-library "emojify") (file-name-directory (locate-library "emojify")))
|
||
(t default-directory))))))
|
||
|
||
(defcustom emojify-emoji-set "emojione-v2.2.6-22"
|
||
"The emoji set used to display emojis."
|
||
:type (append '(radio :tag "Emoji set")
|
||
(mapcar (lambda (set) (list 'const set))
|
||
(ht-keys emojify-emoji-set-json)))
|
||
:group 'emojify)
|
||
|
||
(defcustom emojify-emojis-dir
|
||
(locate-user-emacs-file "emojis")
|
||
"Path to the directory containing the emoji images."
|
||
:type 'directory
|
||
:group 'emojify)
|
||
|
||
(defcustom emojify-display-style
|
||
'image
|
||
"How the emoji's be displayed.
|
||
|
||
Possible values are
|
||
`image' - Display emojis using images, this requires images are supported by
|
||
user's Emacs installation
|
||
`unicode' - Display emojis using unicode characters, this works well on
|
||
platforms with good emoji fonts. In this case the emoji text
|
||
':wink:' will be substituted with 😉.
|
||
`ascii' - Display emojis as ascii characters, this is simplest and does not
|
||
require any external dependencies. In this cases emoji text like
|
||
':wink:' are substituted with ascii equivalents like ';)'"
|
||
:type '(radio :tag "Emoji display style"
|
||
(const :tag "Display emojis as images" image)
|
||
(const :tag "Display emojis as unicode characters" unicode)
|
||
(const :tag "Display emojis as ascii string" ascii))
|
||
:group 'emojify)
|
||
|
||
|
||
|
||
;; Customizations to control the enabling of emojify-mode
|
||
|
||
(defcustom emojify-inhibit-major-modes
|
||
'(dired-mode
|
||
doc-view-mode
|
||
debugger-mode
|
||
pdf-view-mode
|
||
image-mode
|
||
help-mode
|
||
ibuffer-mode
|
||
magit-popup-mode
|
||
magit-diff-mode
|
||
ert-results-mode
|
||
compilation-mode
|
||
proced-mode
|
||
mu4e-headers-mode)
|
||
"Major modes where emojify mode should not be enabled."
|
||
:type '(repeat symbol)
|
||
:group 'emojify)
|
||
|
||
(defcustom emojify-inhibit-in-buffer-functions
|
||
'(emojify-minibuffer-p emojify-helm-buffer-p)
|
||
"Functions used inhibit emojify-mode in a buffer.
|
||
|
||
These functions are called with one argument, the buffer where command
|
||
‘emojify-mode’ is about to be enabled, emojify is not enabled if any of the
|
||
functions return a non-nil value."
|
||
:type 'hook
|
||
:group 'emojify)
|
||
|
||
(defvar emojify-inhibit-emojify-in-current-buffer-p nil
|
||
"Should emojify be inhibited in current buffer.
|
||
|
||
This is a buffer local variable that can be set to inhibit enabling of
|
||
emojify in a buffer.")
|
||
(make-variable-buffer-local 'emojify-inhibit-emojify-in-current-buffer-p)
|
||
|
||
(defvar emojify-minibuffer-reading-emojis-p nil
|
||
"Are we currently reading emojis using minibuffer?")
|
||
|
||
(defun emojify-ephemeral-buffer-p (buffer)
|
||
"Determine if BUFFER is an ephemeral/temporary buffer."
|
||
(and (not (minibufferp))
|
||
(string-match-p "^ " (buffer-name buffer))))
|
||
|
||
(defun emojify-inhibit-major-mode-p (buffer)
|
||
"Determine if user has disabled the `major-mode' enabled for the BUFFER.
|
||
|
||
Returns non-nil if the buffer's major mode is part of `emojify-inhibit-major-modes'"
|
||
(with-current-buffer buffer
|
||
(apply #'derived-mode-p emojify-inhibit-major-modes)))
|
||
|
||
(defun emojify-helm-buffer-p (buffer)
|
||
"Determine if the current BUFFER is a helm buffer."
|
||
(unless emojify-minibuffer-reading-emojis-p
|
||
(string-match-p "\\*helm" (buffer-name buffer))))
|
||
|
||
(defun emojify-minibuffer-p (buffer)
|
||
"Determine if the current BUFFER is a minibuffer."
|
||
(unless emojify-minibuffer-reading-emojis-p
|
||
(minibufferp buffer)))
|
||
|
||
(defun emojify-buffer-p (buffer)
|
||
"Determine if `emojify-mode' should be enabled for given BUFFER.
|
||
|
||
`emojify-mode' mode is not enabled in temporary buffers. Additionally user
|
||
can customize `emojify-inhibit-major-modes' and
|
||
`emojify-inhibit-in-buffer-functions' to disabled emojify in additional buffers."
|
||
(not (or emojify-inhibit-emojify-in-current-buffer-p
|
||
(emojify-ephemeral-buffer-p (current-buffer))
|
||
(emojify-inhibit-major-mode-p (current-buffer))
|
||
(buffer-base-buffer buffer)
|
||
(run-hook-with-args-until-success 'emojify-inhibit-in-buffer-functions buffer))))
|
||
|
||
|
||
|
||
;; Customizations to control display of emojis
|
||
|
||
(defvar emojify-emoji-style-change-hook nil
|
||
"Hooks run when emoji style changes.")
|
||
|
||
;;;###autoload
|
||
(defun emojify-set-emoji-styles (styles)
|
||
"Set the type of emojis that should be displayed.
|
||
|
||
STYLES is the styles emoji styles that should be used, see `emojify-emoji-styles'"
|
||
(when (not (listp styles))
|
||
(setq styles (list styles))
|
||
(warn "`emojify-emoji-style' has been deprecated use `emojify-emoji-styles' instead!"))
|
||
|
||
(setq-default emojify-emoji-styles styles)
|
||
|
||
(run-hooks 'emojify-emoji-style-change-hook))
|
||
|
||
(defcustom emojify-emoji-styles
|
||
'(ascii unicode github)
|
||
"The type of emojis that should be displayed.
|
||
|
||
These can have one of the following values
|
||
|
||
`ascii' - Display only ascii emojis for example ';)'
|
||
`unicode' - Display only unicode emojis for example '😉'
|
||
`github' - Display only github style emojis for example ':wink:'"
|
||
:type '(set
|
||
(const :tag "Display only ascii emojis" ascii)
|
||
(const :tag "Display only github emojis" github)
|
||
(const :tag "Display only unicode codepoints" unicode))
|
||
:set (lambda (_ value) (emojify-set-emoji-styles value))
|
||
:group 'emojify)
|
||
|
||
(defcustom emojify-program-contexts
|
||
'(comments string code)
|
||
"Contexts where emojis can be displayed in programming modes.
|
||
|
||
Possible values are
|
||
`comments' - Display emojis in comments
|
||
`string' - Display emojis in strings
|
||
`code' - Display emojis in code (this is applicable only for unicode emojis)"
|
||
:type '(set :tag "Contexts where emojis should be displayed in programming modes"
|
||
(const :tag "Display emojis in comments" comments)
|
||
(const :tag "Display emojis in string" string)
|
||
(const :tag "Display emojis in code" code))
|
||
:group 'emojify)
|
||
|
||
(defcustom emojify-inhibit-functions
|
||
'(emojify-in-org-tags-p emojify-in-org-list-p)
|
||
"Functions used to determine given emoji should displayed at current point.
|
||
|
||
These functions are called with 3 arguments, the text to be emojified, the start
|
||
of emoji text and the end of emoji text. These functions are called with the
|
||
buffer where emojis are going to be displayed selected."
|
||
:type 'hook
|
||
:group 'emojify)
|
||
|
||
(defcustom emojify-composed-text-p t
|
||
"Should composed text be emojified."
|
||
:type 'boolean
|
||
:group 'emojify)
|
||
|
||
(defcustom emojify-company-tooltips-p t
|
||
"Should company mode tooltips be emojified."
|
||
:type 'boolean
|
||
:group 'emojify)
|
||
|
||
(defun emojify-in-org-tags-p (match beg _end)
|
||
"Determine whether the point is on `org-mode' tag.
|
||
|
||
MATCH, BEG and _END are the text currently matched emoji and the start position
|
||
and end position of emoji text respectively.
|
||
|
||
Easiest would have to inspect face at point but unfortunately, there is no
|
||
way to guarantee that we run after font-lock"
|
||
(and (memq major-mode '(org-mode org-agenda-mode))
|
||
(string-match-p ":[^:]+[:]?" match)
|
||
(org-at-heading-p)
|
||
(save-excursion
|
||
(save-match-data
|
||
(goto-char beg)
|
||
(looking-at ":[^:]+:[\s-]*$")))))
|
||
|
||
(defun emojify-in-org-list-p (text beg &rest ignored)
|
||
"Determine whether the point is in `org-mode' list.
|
||
|
||
TEXT is the text which is supposed to rendered a an emoji. BEG is the beginning
|
||
of the emoji text in the buffer. The arguments IGNORED are ignored."
|
||
(and (eq major-mode 'org-mode)
|
||
(equal text "8)")
|
||
(equal (org-list-get-item-begin) beg)))
|
||
|
||
(defun emojify-valid-program-context-p (emoji beg end)
|
||
"Determine if EMOJI should be displayed for text between BEG and END.
|
||
|
||
This returns non-nil if the region is valid according to `emojify-program-contexts'"
|
||
(when emojify-program-contexts
|
||
(let* ((syntax-beg (syntax-ppss beg))
|
||
(syntax-end (syntax-ppss end))
|
||
(context (cond ((and (nth 3 syntax-beg)
|
||
(nth 3 syntax-end))
|
||
'string)
|
||
((and (nth 4 syntax-beg)
|
||
(nth 4 syntax-end))
|
||
'comments)
|
||
(t 'code))))
|
||
(and (memql context emojify-program-contexts)
|
||
(if (equal context 'code)
|
||
(and (string= (ht-get emoji "style") "unicode")
|
||
(memql 'unicode emojify-emoji-styles))
|
||
t)))))
|
||
|
||
(defun emojify-inside-org-src-p (point)
|
||
"Return non-nil if POINT is inside `org-mode' src block.
|
||
|
||
This is used to inhibit display of emoji's in `org-mode' src blocks
|
||
since our mechanisms do not work in it."
|
||
(when (eq major-mode 'org-mode)
|
||
(save-excursion
|
||
(goto-char point)
|
||
(eq (org-element-type (org-element-at-point)) 'src-block))))
|
||
|
||
(defun emojify-looking-at-end-of-list-maybe (point)
|
||
"Determine if POINT is end of a list.
|
||
|
||
This is not accurate since it restricts the region to scan to
|
||
the visible area."
|
||
(let* ((area (emojify--get-relevant-region))
|
||
(beg (car area))
|
||
(end (cdr area)))
|
||
(save-restriction
|
||
(narrow-to-region beg end)
|
||
(let ((list-start (ignore-errors (scan-sexps point -1))))
|
||
(when (and list-start
|
||
;; Ignore the starting brace if it is an emoji
|
||
(not (get-text-property list-start 'emojified)))
|
||
;; If we got a list start make sure both start and end
|
||
;; belong to same string/comment
|
||
(let ((syntax-beg (syntax-ppss list-start))
|
||
(syntax-end (syntax-ppss point)))
|
||
(and list-start
|
||
(eq (nth 8 syntax-beg)
|
||
(nth 8 syntax-end)))))))))
|
||
|
||
(defun emojify-valid-ascii-emoji-context-p (beg end)
|
||
"Determine if the okay to display ascii emoji between BEG and END."
|
||
;; The text is at the start of the buffer
|
||
(and (or (not (char-before beg))
|
||
;; 32 space since ? (? followed by a space) is not readable
|
||
;; 34 is " since?" confuses font-lock
|
||
;; 41 is ) since?) (extra paren) confuses most packages
|
||
(memq (char-syntax (char-before beg))
|
||
;; space
|
||
'(32
|
||
;; start/end of string
|
||
34
|
||
;; whitespace syntax
|
||
?-
|
||
;; comment start
|
||
?<
|
||
;; comment end, this handles text at start of line immediately
|
||
;; after comment line in a multiline comment
|
||
?>)))
|
||
;; The text is at the end of the buffer
|
||
(or (not (char-after end))
|
||
(memq (char-syntax (char-after end))
|
||
;; space
|
||
'(32
|
||
;; start/end of string
|
||
34
|
||
;; whitespace syntax
|
||
?-
|
||
;; punctuation
|
||
?.
|
||
;; closing braces
|
||
41
|
||
;; comment end
|
||
?>)))))
|
||
|
||
|
||
|
||
;; Obsolete vars
|
||
|
||
(define-obsolete-variable-alias 'emojify-emoji-style 'emojify-emoji-styles "0.2")
|
||
(define-obsolete-function-alias 'emojify-set-emoji-style 'emojify-set-emoji-styles "0.2")
|
||
|
||
|
||
|
||
;; Customizations to control the behaviour when point enters emojified text
|
||
|
||
(defcustom emojify-point-entered-behaviour 'echo
|
||
"The behaviour when point enters, an emojified text.
|
||
|
||
It can be one of the following
|
||
`echo' - Echo the underlying text in the minibuffer
|
||
`uncover' - Display the underlying text while point is on it
|
||
function - It is called with 2 arguments (the buffer where emoji appears is
|
||
current during execution)
|
||
1) starting position of emoji text
|
||
2) ending position of emoji text
|
||
|
||
Does nothing if the value is anything else."
|
||
;; TODO: Mention custom function
|
||
:type '(radio :tag "Behaviour when point enters an emoji"
|
||
(const :tag "Echo the underlying emoji text in the minibuffer" echo)
|
||
(const :tag "Uncover (undisplay) the underlying emoji text" uncover))
|
||
:group 'emojify)
|
||
|
||
(defcustom emojify-reveal-on-isearch t
|
||
"Should underlying emoji be displayed when point enters emoji while in isearch mode.")
|
||
|
||
(defcustom emojify-show-help t
|
||
"If non-nil the underlying text is displayed in a popup when mouse moves over it."
|
||
:type 'boolean
|
||
:group 'emojify)
|
||
|
||
(defun emojify-on-emoji-enter (beginning end)
|
||
"Executed when point enters emojified text between BEGINNING and END."
|
||
(cond ((and (eq emojify-point-entered-behaviour 'echo)
|
||
;; Do not echo in isearch-mode
|
||
(not isearch-mode)
|
||
(not (active-minibuffer-window))
|
||
(not (current-message)))
|
||
(message (substring-no-properties (get-text-property beginning 'emojify-text))))
|
||
((eq emojify-point-entered-behaviour 'uncover)
|
||
(put-text-property beginning end 'display nil))
|
||
((functionp 'emojify-point-entered-behaviour)
|
||
(funcall emojify-point-entered-behaviour beginning end)))
|
||
|
||
(when (and isearch-mode emojify-reveal-on-isearch)
|
||
(put-text-property beginning end 'display nil)))
|
||
|
||
(defun emojify-on-emoji-exit (beginning end)
|
||
"Executed when point exits emojified text between BEGINNING and END."
|
||
(put-text-property beginning
|
||
end
|
||
'display
|
||
(get-text-property beginning 'emojify-display)))
|
||
|
||
(defvar-local emojify--last-emoji-pos nil)
|
||
|
||
(defun emojify-detect-emoji-entry/exit ()
|
||
"Detect emoji entry and exit and run appropriate handlers.
|
||
|
||
This is inspired by `prettify-symbol-mode's logic for
|
||
`prettify-symbols-unprettify-at-point'."
|
||
(while-no-input
|
||
(emojify-with-saved-buffer-state
|
||
(when emojify--last-emoji-pos
|
||
(emojify-on-emoji-exit (car emojify--last-emoji-pos) (cdr emojify--last-emoji-pos)))
|
||
|
||
(when (get-text-property (point) 'emojified)
|
||
(let* ((text-props (text-properties-at (point)))
|
||
(buffer (plist-get text-props 'emojify-buffer))
|
||
(match-beginning (plist-get text-props 'emojify-beginning))
|
||
(match-end (plist-get text-props 'emojify-end)))
|
||
(when (eq buffer (current-buffer))
|
||
(emojify-on-emoji-enter match-beginning match-end)
|
||
(setq emojify--last-emoji-pos (cons match-beginning match-end))))))))
|
||
|
||
(defun emojify-help-function (_window _string pos)
|
||
"Function to get help string to be echoed when point/mouse into the point.
|
||
|
||
To understand WINDOW, STRING and POS see the function documentation for
|
||
`help-echo' text-property."
|
||
(when (and emojify-show-help
|
||
(not isearch-mode)
|
||
(not (active-minibuffer-window))
|
||
(not (current-message)))
|
||
(plist-get (text-properties-at pos) 'emojify-text)))
|
||
|
||
|
||
|
||
;; Core functions and macros
|
||
|
||
;; Variables related to user emojis
|
||
|
||
(defcustom emojify-user-emojis nil
|
||
"User specified custom emojis.
|
||
|
||
This is an alist where first element of cons is the text to be displayed as
|
||
emoji, while the second element of the cons is an alist containing data about
|
||
the emoji.
|
||
|
||
The inner alist should have atleast (not all keys are strings)
|
||
|
||
`name' - The name of the emoji
|
||
`style' - This should be one of \"github\", \"ascii\" or \"github\"
|
||
(see `emojify-emoji-styles')
|
||
|
||
The alist should contain one of (see `emojify-display-style')
|
||
`unicode' - The replacement for the provided emoji for \"unicode\" display style
|
||
`image' - The replacement for the provided emoji for \"image\" display style.
|
||
This should be the absolute path to the image
|
||
`ascii' - The replacement for the provided emoji for \"ascii\" display style
|
||
|
||
Example -
|
||
The following assumes that custom images are at ~/.emacs.d/emojis/trollface.png and
|
||
~/.emacs.d/emojis/neckbeard.png
|
||
|
||
'((\":troll:\" . ((\"name\" . \"Troll\")
|
||
(\"image\" . \"~/.emacs.d/emojis/trollface.png\")
|
||
(\"style\" . \"github\")))
|
||
(\":neckbeard:\" . ((\"name\" . \"Neckbeard\")
|
||
(\"image\" . \"~/.emacs.d/emojis/neckbeard.png\")
|
||
(\"style\" . \"github\"))))")
|
||
|
||
(defvar emojify--user-emojis nil
|
||
"User specified custom emojis.")
|
||
|
||
(defvar emojify--user-emojis-regexp nil
|
||
"Regexp to match user specified custom emojis.")
|
||
|
||
;; Variables related to default emojis
|
||
(defvar emojify-emojis nil
|
||
"Data about the emojis, this contains only the emojis that come with emojify.")
|
||
|
||
(defvar emojify-regexps nil
|
||
"List of regexps to match text to be emojified.")
|
||
|
||
(defvar emojify--completing-candidates-cache nil
|
||
"Cached values for completing read candidates calculated for `emojify-completing-read'.")
|
||
|
||
;; Cache for emoji completing read candidates
|
||
(defun emojify--get-completing-read-candidates ()
|
||
"Get the candidates to be used for `emojify-completing-read'.
|
||
|
||
The candidates are calculated according to currently active
|
||
`emojify-emoji-styles' and cached"
|
||
(let ((styles (mapcar #'symbol-name emojify-emoji-styles)))
|
||
(unless (and emojify--completing-candidates-cache
|
||
(equal styles (car emojify--completing-candidates-cache)))
|
||
(setq emojify--completing-candidates-cache
|
||
(cons styles
|
||
(let ((emojis (ht-create #'equal)))
|
||
(emojify-emojis-each (lambda (key value)
|
||
(when (seq-position styles (ht-get value "style"))
|
||
(ht-set! emojis
|
||
(format "%s - %s (%s)"
|
||
key
|
||
(ht-get value "name")
|
||
(ht-get value "style"))
|
||
value))))
|
||
emojis))))
|
||
(cdr emojify--completing-candidates-cache)))
|
||
|
||
(defun emojify-create-emojify-emojis (&optional force)
|
||
"Create `emojify-emojis' if needed.
|
||
|
||
The function avoids reading emoji data if it has already been read unless FORCE
|
||
in which case emoji data is re-read."
|
||
(when (or force (not emojify-emojis))
|
||
(emojify-set-emoji-data)))
|
||
|
||
(defun emojify-get-emoji (emoji)
|
||
"Get data for given EMOJI.
|
||
|
||
This first looks for the emoji in `emojify--user-emojis',
|
||
and then in `emojify-emojis'."
|
||
(or (when emojify--user-emojis
|
||
(ht-get emojify--user-emojis emoji))
|
||
(ht-get emojify-emojis emoji)))
|
||
|
||
(defun emojify-emojis-each (function)
|
||
"Execute FUNCTION for each emoji.
|
||
|
||
This first runs function for `emojify--user-emojis',
|
||
and then `emojify-emojis'."
|
||
(when emojify--user-emojis
|
||
(ht-each function emojify--user-emojis))
|
||
(ht-each function emojify-emojis))
|
||
|
||
(defun emojify--verify-user-emojis (emojis)
|
||
"Verify the EMOJIS in correct user format."
|
||
(seq-every-p (lambda (emoji)
|
||
(and (assoc "name" (cdr emoji))
|
||
;; Make sure style is present is only one of
|
||
;; "unicode", "ascii" and "github".
|
||
(assoc "style" (cdr emoji))
|
||
(seq-position '("unicode" "ascii" "github")
|
||
(cdr (assoc "style" (cdr emoji))))
|
||
(or (assoc "unicode" (cdr emoji))
|
||
(assoc "image" (cdr emoji))
|
||
(assoc "ascii" (cdr emoji)))))
|
||
emojis))
|
||
|
||
(defun emojify-set-emoji-data ()
|
||
"Read the emoji data for STYLES and set the regexp required to search them."
|
||
(setq-default emojify-emojis (let ((json-array-type 'list)
|
||
(json-object-type 'hash-table))
|
||
(json-read-file emojify-emoji-json)))
|
||
|
||
(let (unicode-emojis ascii-emojis)
|
||
(ht-each (lambda (emoji data)
|
||
(when (string= (ht-get data "style") "unicode")
|
||
(push emoji unicode-emojis))
|
||
|
||
(when (string= (ht-get data "style") "ascii")
|
||
(push emoji ascii-emojis)))
|
||
emojify-emojis)
|
||
|
||
;; Construct emojify-regexps such that github style are searched first
|
||
;; followed by unicode and then ascii emojis.
|
||
(setq emojify-regexps (list ":[[:alnum:]+_-]+:"
|
||
(regexp-opt unicode-emojis)
|
||
(regexp-opt ascii-emojis))))
|
||
|
||
(when emojify-user-emojis
|
||
(if (emojify--verify-user-emojis emojify-user-emojis)
|
||
;; Create entries for user emojis
|
||
(let ((emoji-pairs (mapcar (lambda (user-emoji)
|
||
(cons (car user-emoji)
|
||
(ht-from-alist (cdr user-emoji))))
|
||
emojify-user-emojis)))
|
||
(setq emojify--user-emojis (ht-from-alist emoji-pairs))
|
||
(setq emojify--user-emojis-regexp (regexp-opt (mapcar #'car emoji-pairs))))
|
||
(message "[emojify] User emojis are not in correct format ignoring them.")))
|
||
|
||
(emojify-emojis-each (lambda (emoji data)
|
||
;; Add the emoji text to data, this makes the values
|
||
;; of the `emojify-emojis' standalone containing all
|
||
;; data about the emoji
|
||
(ht-set! data "emoji" emoji)
|
||
(ht-set! data "custom" (and emojify--user-emojis
|
||
(ht-get emojify--user-emojis emoji)))))
|
||
|
||
;; Clear completion candidates cache
|
||
(setq emojify--completing-candidates-cache nil))
|
||
|
||
(defvar emojify-emoji-keymap
|
||
(let ((map (make-sparse-keymap)))
|
||
(define-key map [remap delete-char] #'emojify-delete-emoji-forward)
|
||
(define-key map [remap delete-forward-char] #'emojify-delete-emoji-forward)
|
||
(define-key map [remap backward-delete-char] #'emojify-delete-emoji-backward)
|
||
(define-key map [remap org-delete-backward-char] #'emojify-delete-emoji-backward)
|
||
(define-key map [remap delete-backward-char] #'emojify-delete-emoji-backward)
|
||
(define-key map [remap backward-delete-char-untabify] #'emojify-delete-emoji-backward)
|
||
map))
|
||
|
||
(defun emojify-image-dir ()
|
||
"Get the path to directory containing images for currently selected emoji set."
|
||
(expand-file-name emojify-emoji-set
|
||
emojify-emojis-dir))
|
||
|
||
(defun emojify--get-point-col-and-line (point)
|
||
"Return a cons of containing the column number and line at POINT."
|
||
(save-excursion
|
||
(goto-char point)
|
||
(cons (current-column) (line-number-at-pos))))
|
||
|
||
(defun emojify--get-composed-text (point)
|
||
"Get the text used as composition property at POINT.
|
||
|
||
This does not check if there is composition property at point the callers should
|
||
make sure the point has a composition property otherwise this function will
|
||
fail."
|
||
(emojify--string-join (mapcar #'char-to-string
|
||
(decode-composition-components (nth 2
|
||
(find-composition point
|
||
nil
|
||
nil
|
||
t))))))
|
||
|
||
(defun emojify--inside-rectangle-selection-p (beg end)
|
||
"Check if region marked by BEG and END is inside a rectangular selection.
|
||
|
||
In addition to explicit the parameters BEG and END, calling functions should
|
||
also dynamically bind `emojify-region-beg' and `emojify-region-end' to beginning
|
||
and end of region respectively."
|
||
(when (and emojify-region-beg
|
||
(bound-and-true-p rectangle-mark-mode))
|
||
(let ((rect-beg (emojify--get-point-col-and-line emojify-region-beg))
|
||
(rect-end (emojify--get-point-col-and-line emojify-region-end))
|
||
(emoji-start-pos (emojify--get-point-col-and-line beg))
|
||
(emoji-end-pos (emojify--get-point-col-and-line end)))
|
||
(or (and (<= (car rect-beg) (car emoji-start-pos))
|
||
(<= (car emoji-start-pos) (car rect-end))
|
||
(<= (cdr rect-beg) (cdr emoji-start-pos))
|
||
(<= (cdr emoji-start-pos) (cdr rect-end)))
|
||
(and (<= (car rect-beg) (car emoji-end-pos))
|
||
(<= (car emoji-end-pos) (car rect-end))
|
||
(<= (cdr rect-beg) (cdr emoji-end-pos))
|
||
(<= (cdr emoji-end-pos) (cdr rect-end)))))))
|
||
|
||
(defun emojify--inside-non-rectangle-selection-p (beg end)
|
||
"Check if region marked by BEG and END is inside a non-regular selection.
|
||
|
||
In addition to the explicit parameters BEG and END, calling functions should
|
||
also dynamically bind `emojify-region-beg' and `emojify-region-end' to beginning
|
||
and end of region respectively."
|
||
(when (and emojify-region-beg
|
||
(region-active-p)
|
||
(not (bound-and-true-p rectangle-mark-mode)))
|
||
(or (and (< emojify-region-beg beg)
|
||
(<= beg emojify-region-end))
|
||
(and (< emojify-region-beg end)
|
||
(<= end emojify-region-end)))))
|
||
|
||
(defun emojify--region-background-maybe (beg end)
|
||
"If the BEG and END falls inside an active region return the region face.
|
||
|
||
This returns nil if the emojis between BEG and END do not fall in region."
|
||
;; `redisplay-highlight-region-function' was not defined in Emacs 24.3
|
||
(when (and (or (not (boundp 'redisplay-highlight-region-function))
|
||
(equal (default-value 'redisplay-highlight-region-function) redisplay-highlight-region-function))
|
||
(or (emojify--inside-non-rectangle-selection-p beg end)
|
||
(emojify--inside-rectangle-selection-p beg end)))
|
||
(face-background 'region)))
|
||
|
||
(defun emojify--get-image-background (beg end)
|
||
"Get the color to be used as background for emoji between BEG and END."
|
||
;; We do a separate check for region since `background-color-at-point'
|
||
;; does not always detect background color inside regions properly
|
||
(or (emojify--region-background-maybe beg end)
|
||
(save-excursion
|
||
(goto-char beg)
|
||
(background-color-at-point))))
|
||
|
||
(defvar emojify--imagemagick-support-cache (ht-create))
|
||
|
||
(defun emojify--imagemagick-supports-p (format)
|
||
"Check if imagemagick support given FORMAT.
|
||
|
||
This function caches the result of the check since the naive check
|
||
|
||
(memq format (imagemagick-types))
|
||
|
||
can be expensive if imagemagick-types returns a large list, this is
|
||
especially problematic since this check is potentially called during
|
||
very redisplay. See https://github.com/iqbalansari/emacs-emojify/issues/41"
|
||
(when (fboundp 'imagemagick-types)
|
||
(when (equal (ht-get emojify--imagemagick-support-cache format 'unset) 'unset)
|
||
(ht-set emojify--imagemagick-support-cache format (memq format (imagemagick-types))))
|
||
(ht-get emojify--imagemagick-support-cache format)))
|
||
|
||
(defun emojify--get-image-display (data buffer beg end &optional target)
|
||
"Get the display text property to display the emoji as an image.
|
||
|
||
DATA holds the emoji data, _BUFFER is the target buffer where the emoji is to be
|
||
displayed, BEG and END delimit the region where emoji will be displayed. For
|
||
explanation of TARGET see the documentation of `emojify--get-text-display-props'.
|
||
|
||
TODO: Perhaps TARGET should be generalized to work with overlays, buffers and
|
||
other different display constructs, for now this works."
|
||
(when (ht-get data "image")
|
||
(let* ((image-file (expand-file-name (ht-get data "image")
|
||
(emojify-image-dir)))
|
||
(image-type (intern (upcase (file-name-extension image-file)))))
|
||
(when (file-exists-p image-file)
|
||
(create-image image-file
|
||
;; use imagemagick if available and supports PNG images
|
||
;; (allows resizing images)
|
||
(when (emojify--imagemagick-supports-p image-type)
|
||
'imagemagick)
|
||
nil
|
||
:ascent 'center
|
||
:heuristic-mask t
|
||
:background (cond ((equal target 'mode-line)
|
||
(face-background 'mode-line nil 'default))
|
||
(t (emojify--get-image-background beg end)))
|
||
;; no-op if imagemagick is not available
|
||
:height (cond ((bufferp target)
|
||
(with-current-buffer target
|
||
(emojify-default-font-height)))
|
||
((equal target 'mode-line)
|
||
(emojify-face-height 'mode-line))
|
||
(t (with-current-buffer buffer
|
||
(emojify-default-font-height)))))))))
|
||
|
||
(defun emojify--get-unicode-display (data &rest ignored)
|
||
"Get the display text property to display the emoji as an unicode character.
|
||
|
||
DATA holds the emoji data, rest of the arguments IGNORED are ignored"
|
||
(let* ((unicode (ht-get data "unicode"))
|
||
(characters (when unicode
|
||
(string-to-vector unicode))))
|
||
(when (seq-every-p #'char-displayable-p characters)
|
||
unicode)))
|
||
|
||
(defun emojify--get-ascii-display (data &rest ignored)
|
||
"Get the display text property to display the emoji as an ascii characters.
|
||
|
||
DATA holds the emoji data, rest of the arguments IGNORED are ignored."
|
||
(ht-get data "ascii"))
|
||
|
||
(defun emojify--get-text-display-props (emoji buffer beg end &optional target)
|
||
"Get the display property for an EMOJI.
|
||
|
||
BUFFER is the buffer currently holding the EMOJI, BEG and END delimit the region
|
||
containing the emoji. TARGET can either be a buffer object or a special value
|
||
mode-line. It is used to indicate where EMOJI would be displayed, properties
|
||
like font-height are inherited from TARGET if provided."
|
||
(funcall (pcase emojify-display-style
|
||
(`image #'emojify--get-image-display)
|
||
(`unicode #'emojify--get-unicode-display)
|
||
(`ascii #'emojify--get-ascii-display))
|
||
emoji
|
||
buffer
|
||
beg
|
||
end
|
||
target))
|
||
|
||
(defun emojify--propertize-text-for-emoji (emoji text buffer start end &optional target)
|
||
"Display EMOJI for TEXT in BUFFER between START and END.
|
||
|
||
For explanation of TARGET see the documentation of
|
||
`emojify--get-text-display-props'."
|
||
(let ((display-prop
|
||
(emojify--get-text-display-props emoji buffer start end target))
|
||
(buffer-props (unless target
|
||
(list 'emojify-buffer buffer
|
||
'emojify-beginning (copy-marker start)
|
||
'emojify-end (copy-marker end)
|
||
'yank-handler (list nil text)
|
||
'keymap emojify-emoji-keymap
|
||
'help-echo #'emojify-help-function))))
|
||
(when display-prop
|
||
(add-text-properties start
|
||
end
|
||
(append (list 'emojified t
|
||
'emojify-display display-prop
|
||
'display display-prop
|
||
'emojify-text text)
|
||
buffer-props)))))
|
||
|
||
(defun emojify-display-emojis-in-region (beg end)
|
||
"Display emojis in region.
|
||
|
||
BEG and END are the beginning and end of the region respectively.
|
||
|
||
Displaying happens in two phases, first search based phase displays actual text
|
||
appearing in buffer as emojis. In the next phase composed text is searched for
|
||
emojis and displayed.
|
||
|
||
A minor problem here is that if the text is composed after this display loop it
|
||
would not be displayed as emoji, although in practice the two packages that use
|
||
the composition property `prettify-symbol-mode' and `org-bullets' use the
|
||
font-lock machinery which runs before emojify's display loop, so hopefully this
|
||
should not be a problem 🤞."
|
||
(emojify-with-saved-buffer-state
|
||
;; Make sure we halt if displaying emojis takes more than a second (this
|
||
;; might be too large duration)
|
||
(with-timeout (1 (emojify-message "Failed to display emojis under 1 second"))
|
||
(seq-doseq (regexp (apply #'append
|
||
(when emojify--user-emojis-regexp
|
||
(list emojify--user-emojis-regexp))
|
||
(list emojify-regexps)))
|
||
(let (case-fold-search)
|
||
(goto-char beg)
|
||
(while (and (> end (point))
|
||
(search-forward-regexp regexp end t))
|
||
(let* ((match-beginning (match-beginning 0))
|
||
(match-end (match-end 0))
|
||
(match (match-string-no-properties 0))
|
||
(buffer (current-buffer))
|
||
(emoji (emojify-get-emoji match))
|
||
(force-display (get-text-property match-beginning 'emojify-force-display)))
|
||
(when (and emoji
|
||
(not (or (get-text-property match-beginning 'emojify-inhibit)
|
||
(get-text-property match-end 'emojify-inhibit)))
|
||
(memql (intern (ht-get emoji "style"))
|
||
emojify-emoji-styles)
|
||
;; Skip displaying this emoji if the its bounds are
|
||
;; already part of an existing emoji. Since the emojis
|
||
;; are searched in descending order of length (see
|
||
;; construction of emojify-regexp in `emojify-set-emoji-data'),
|
||
;; this means larger emojis get precedence over smaller
|
||
;; ones
|
||
(not (or (get-text-property match-beginning 'emojified)
|
||
(get-text-property (1- match-end) 'emojified)))
|
||
;; Display unconditionally in non-prog mode
|
||
(or (not (derived-mode-p 'prog-mode 'tuareg--prog-mode 'comint-mode 'smalltalk-mode))
|
||
;; In prog mode enable respecting `emojify-program-contexts'
|
||
(emojify-valid-program-context-p emoji match-beginning match-end))
|
||
|
||
;; Display ascii emojis conservatively, since they have potential
|
||
;; to be annoying consider d: in head:, except while executing apropos
|
||
;; emoji
|
||
(or (not (string= (ht-get emoji "style") "ascii"))
|
||
force-display
|
||
(emojify-valid-ascii-emoji-context-p match-beginning match-end))
|
||
|
||
(or force-display
|
||
(not (emojify-inside-org-src-p match-beginning)))
|
||
|
||
;; Inhibit possibly inside a list
|
||
;; 41 is ?) but packages get confused by the extra closing paren :)
|
||
;; TODO Report bugs to such packages
|
||
(or force-display
|
||
(not (and (eq (char-syntax (char-before match-end)) 41)
|
||
(emojify-looking-at-end-of-list-maybe match-end))))
|
||
|
||
(not (run-hook-with-args-until-success 'emojify-inhibit-functions match match-beginning match-end)))
|
||
(emojify--propertize-text-for-emoji emoji match buffer match-beginning match-end)))
|
||
;; Stop a bit to let `with-timeout' kick in
|
||
(sit-for 0 t))))
|
||
|
||
;; Loop to emojify composed text
|
||
(when (and emojify-composed-text-p
|
||
;; Skip this if user has disabled unicode style emojis, since
|
||
;; we display only composed text that are unicode emojis
|
||
(memql 'unicode emojify-emoji-styles))
|
||
(goto-char beg)
|
||
(let ((compose-start (if (get-text-property beg 'composition)
|
||
;; Check `beg' first for composition property
|
||
;; since `next-single-property-change' will
|
||
;; search for region after `beg' for property
|
||
;; change thus skipping any composed text at
|
||
;; `beg'
|
||
beg
|
||
(next-single-property-change beg
|
||
'composition
|
||
nil
|
||
end))))
|
||
(while (and (> end (point))
|
||
;; `end' would be equal to `compose-start' if there was no
|
||
;; text with composition found within `end', this happens
|
||
;; because `next-single-property-change' returns the limit
|
||
;; (and we use `end' as the limit) if no match is found
|
||
(> end compose-start)
|
||
compose-start)
|
||
(let* ((match (emojify--get-composed-text compose-start))
|
||
(emoji (emojify-get-emoji match))
|
||
(compose-end (next-single-property-change compose-start 'composition nil end)))
|
||
;; Display only composed text that is unicode char
|
||
(when (and emoji
|
||
(string= (ht-get emoji "style") "unicode"))
|
||
(emojify--propertize-text-for-emoji emoji match (current-buffer) compose-start compose-end))
|
||
;; Setup the next loop
|
||
(setq compose-start (and compose-end (next-single-property-change compose-end
|
||
'composition
|
||
nil
|
||
end)))
|
||
(goto-char compose-end))
|
||
;; Stop a bit to let `with-timeout' kick in
|
||
(sit-for 0 t)))))))
|
||
|
||
(defun emojify-undisplay-emojis-in-region (beg end)
|
||
"Undisplay the emojis in region.
|
||
|
||
BEG and END are the beginning and end of the region respectively"
|
||
(emojify-with-saved-buffer-state
|
||
(while (< beg end)
|
||
;; Get the start of emojified region in the region, the region is marked
|
||
;; with text-property `emojified' whose value is `t'. The region is marked
|
||
;; so that we do not inadvertently remove display or other properties
|
||
;; inserted by other packages. This might fail too if a package adds any
|
||
;; of these properties between an emojified text, but that situation is
|
||
;; hopefully very rare and this is better than blindly removing all text
|
||
;; properties
|
||
(let* ((emoji-start (text-property-any beg end 'emojified t))
|
||
;; Get the end emojified text, if we could not find the start set
|
||
;; emoji-end to region `end', this merely to make looping easier.
|
||
(emoji-end (or (and emoji-start
|
||
(text-property-not-all emoji-start end 'emojified t))
|
||
;; If the emojified text is at the end of the region
|
||
;; assume that end is the emojified text.
|
||
end)))
|
||
;; Proceed only if we got start of emojified text
|
||
(when emoji-start
|
||
;; Remove the properties
|
||
(remove-text-properties emoji-start emoji-end (append (list 'emojified t
|
||
'display t
|
||
'emojify-display t
|
||
'emojify-buffer t
|
||
'emojify-text t
|
||
'emojify-beginning t
|
||
'emojify-end t
|
||
'yank-handler t
|
||
'keymap t
|
||
'help-echo t
|
||
'rear-nonsticky t))))
|
||
;; Setup the next iteration
|
||
(setq beg emoji-end)))))
|
||
|
||
(defun emojify-redisplay-emojis-in-region (&optional beg end)
|
||
"Redisplay emojis in region between BEG and END.
|
||
|
||
Redisplay emojis in the visible region if BEG and END are not specified"
|
||
(let* ((area (emojify--get-relevant-region))
|
||
(beg (save-excursion
|
||
(goto-char (or beg (car area)))
|
||
(line-beginning-position)))
|
||
(end (save-excursion
|
||
(goto-char (or end (cdr area)))
|
||
(line-end-position))))
|
||
(unless (> (- end beg) 5000)
|
||
(emojify-execute-ignoring-errors-unless-debug
|
||
(emojify-undisplay-emojis-in-region beg end)
|
||
(emojify-display-emojis-in-region beg end)))))
|
||
|
||
(defun emojify-after-change-extend-region-function (beg end _len)
|
||
"Extend the region to be emojified.
|
||
|
||
This simply extends the region to be fontified to the start of line at BEG and
|
||
end of line at END. _LEN is ignored.
|
||
|
||
The idea is since an emoji cannot span multiple lines, redisplaying complete
|
||
lines ensures that all the possibly affected emojis are redisplayed."
|
||
(let ((emojify-jit-lock-start (save-excursion
|
||
(goto-char beg)
|
||
(line-beginning-position)))
|
||
(emojify-jit-lock-end (save-excursion
|
||
(goto-char end)
|
||
(line-end-position))))
|
||
(setq jit-lock-start (if jit-lock-start
|
||
(min jit-lock-start emojify-jit-lock-start)
|
||
emojify-jit-lock-start))
|
||
(setq jit-lock-end (if jit-lock-end
|
||
(max jit-lock-end emojify-jit-lock-end)
|
||
emojify-jit-lock-end))))
|
||
|
||
|
||
|
||
;; Emojify standalone strings
|
||
|
||
(defun emojify-string (string &optional styles target)
|
||
"Create a propertized version of STRING, to display emojis belonging STYLES.
|
||
|
||
TARGET can either be a buffer object or a special value mode-line. It is used
|
||
to indicate where EMOJI would be displayed, properties like font-height are
|
||
inherited from TARGET if provided. See also `emojify--get-text-display-props'."
|
||
(emojify-create-emojify-emojis)
|
||
(let ((target (or target (current-buffer))))
|
||
(with-temp-buffer
|
||
(insert string)
|
||
(let ((beg (point-min))
|
||
(end (point-max))
|
||
(styles (or styles '(unicode))))
|
||
(seq-doseq (regexp (apply #'append
|
||
(when emojify--user-emojis-regexp
|
||
(list emojify--user-emojis-regexp))
|
||
(list emojify-regexps)))
|
||
(goto-char beg)
|
||
(while (and (> end (point))
|
||
(search-forward-regexp regexp end t))
|
||
(let* ((match-beginning (match-beginning 0))
|
||
(match-end (match-end 0))
|
||
(match (match-string-no-properties 0))
|
||
(buffer (current-buffer))
|
||
(emoji (emojify-get-emoji match)))
|
||
(when (and emoji
|
||
(not (or (get-text-property match-beginning 'emojify-inhibit)
|
||
(get-text-property match-end 'emojify-inhibit)))
|
||
(memql (intern (ht-get emoji "style")) styles)
|
||
;; Skip displaying this emoji if the its bounds are
|
||
;; already part of an existing emoji. Since the emojis
|
||
;; are searched in descending order of length (see
|
||
;; construction of emojify-regexp in `emojify-set-emoji-data'),
|
||
;; this means larger emojis get precedence over smaller
|
||
;; ones
|
||
(not (or (get-text-property match-beginning 'emojified)
|
||
(get-text-property (1- match-end) 'emojified))))
|
||
(emojify--propertize-text-for-emoji emoji match buffer match-beginning match-end target))))))
|
||
(buffer-string))))
|
||
|
||
|
||
|
||
;; Electric delete functionality
|
||
|
||
(defun emojify--find-key-binding-ignoring-emojify-keymap (key)
|
||
"Find the binding for given KEY ignoring the text properties at point.
|
||
|
||
This is needed since `key-binding' looks up in keymap text property as well
|
||
which is not what we want when falling back in `emojify-delete-emoji'"
|
||
(let* ((key-binding (or (minor-mode-key-binding key)
|
||
(local-key-binding key)
|
||
(global-key-binding key))))
|
||
(when key-binding
|
||
(or (command-remapping key-binding
|
||
nil
|
||
(seq-filter (lambda (keymap)
|
||
(not (equal keymap emojify-emoji-keymap)))
|
||
(current-active-maps)))
|
||
key-binding))))
|
||
|
||
(defun emojify-delete-emoji (point)
|
||
"Delete emoji at POINT."
|
||
(if (get-text-property point 'emojified)
|
||
(delete-region (get-text-property point 'emojify-beginning)
|
||
(get-text-property point 'emojify-end))
|
||
(call-interactively (emojify--find-key-binding-ignoring-emojify-keymap (this-command-keys)))))
|
||
|
||
(defun emojify-delete-emoji-forward ()
|
||
"Delete emoji after point."
|
||
(interactive)
|
||
(emojify-delete-emoji (point)))
|
||
|
||
(defun emojify-delete-emoji-backward ()
|
||
"Delete emoji before point."
|
||
(interactive)
|
||
(emojify-delete-emoji (1- (point))))
|
||
|
||
;; Integrate with delete-selection-mode
|
||
;; Basically instruct delete-selection mode to override our commands
|
||
;; if the region is active.
|
||
(put 'emojify-delete-emoji-forward 'delete-selection 'supersede)
|
||
(put 'emojify-delete-emoji-backward 'delete-selection 'supersede)
|
||
|
||
|
||
|
||
;; Updating background color on selection
|
||
|
||
(defun emojify--update-emojis-background-in-region (&optional beg end)
|
||
"Update the background color for emojis between BEG and END."
|
||
(when (equal emojify-display-style 'image)
|
||
(emojify-with-saved-buffer-state
|
||
(emojify-do-for-emojis-in-region beg end
|
||
(plist-put (cdr (get-text-property emoji-start 'display))
|
||
:background
|
||
(emojify--get-image-background emoji-start
|
||
emoji-end))))))
|
||
|
||
(defun emojify--update-emojis-background-in-region-starting-at (point)
|
||
"Update background color for emojis in buffer starting at POINT.
|
||
|
||
This updates the emojis in the region starting from POINT, the end of region is
|
||
determined by product of `frame-height' and `frame-width' which roughly
|
||
corresponds to the visible area. POINT usually corresponds to the starting
|
||
position of the window, see
|
||
`emojify-update-visible-emojis-background-after-command' and
|
||
`emojify-update-visible-emojis-background-after-window-scroll'
|
||
|
||
NOTE: `window-text-height' and `window-text-width' would have been more
|
||
appropriate here however they were not defined in Emacs v24.3 and below."
|
||
(let* ((region-beginning point)
|
||
(region-end (min (+ region-beginning (* (frame-height)
|
||
(frame-width)))
|
||
(point-max))))
|
||
(emojify--update-emojis-background-in-region region-beginning
|
||
region-end)))
|
||
|
||
(defun emojify-update-visible-emojis-background-after-command ()
|
||
"Function added to `post-command-hook' when region is active.
|
||
|
||
This function updates the backgrounds of the emojis in the region changed after
|
||
the command.
|
||
|
||
Ideally this would have been good enough to update emoji backgounds after region
|
||
changes, unfortunately this does not work well with commands that scroll the
|
||
window specifically `window-start' and `window-end' (sometimes only `window-end')
|
||
report incorrect values.
|
||
|
||
To work around this
|
||
`emojify-update-visible-emojis-background-after-window-scroll' is added to
|
||
`window-scroll-functions' to update emojis on window scroll."
|
||
(while-no-input (emojify--update-emojis-background-in-region-starting-at (window-start))))
|
||
|
||
(defun emojify-update-visible-emojis-background-after-window-scroll (_window display-start)
|
||
"Function added to `window-scroll-functions' when region is active.
|
||
|
||
This function updates the backgrounds of the emojis in the newly displayed area
|
||
of the window. DISPLAY-START corresponds to the new start of the window."
|
||
(while-no-input (emojify--update-emojis-background-in-region-starting-at display-start)))
|
||
|
||
|
||
|
||
;; Lazy image downloading
|
||
|
||
(defcustom emojify-download-emojis-p 'ask
|
||
"Should emojify download images, if the selected emoji sets are not available.
|
||
|
||
Emojify can automatically download the images required to display the selected
|
||
emoji set. By default the user will be asked for confirmation before downloading
|
||
the image. Set this variable to t to download the images without asking for
|
||
confirmation. Setting it to nil will disable automatic download of the images.
|
||
|
||
Please note that emojify will not download the images if Emacs is running in
|
||
non-interactive mode and `emojify-download-emojis-p' is set to `ask'."
|
||
:type '(radio :tag "Automatically download required images"
|
||
(const :tag "Ask before downloading" ask)
|
||
(const :tag "Download without asking" t)
|
||
(const :tag "Disable automatic downloads" nil))
|
||
:group 'emojify)
|
||
|
||
(defvar emojify--refused-image-download-p nil
|
||
"Used to remember that user has refused to download images in this session.")
|
||
(defvar emojify--download-in-progress-p nil
|
||
"Is emoji download in progress used to avoid multiple emoji download prompts.")
|
||
|
||
(defun emojify--emoji-download-emoji-set (data)
|
||
"Download the emoji images according to DATA."
|
||
(let ((destination (expand-file-name (make-temp-name "emojify")
|
||
temporary-file-directory)))
|
||
(url-copy-file (ht-get data "url")
|
||
destination)
|
||
(let ((downloaded-sha (with-temp-buffer
|
||
(insert-file-contents-literally destination)
|
||
(secure-hash 'sha256 (current-buffer)))))
|
||
(when (string= downloaded-sha (ht-get data "sha256"))
|
||
destination))))
|
||
|
||
(defun emojify--extract-emojis (file)
|
||
"Extract the tar FILE in emoji directory."
|
||
(let* ((default-directory emojify-emojis-dir))
|
||
(with-temp-buffer
|
||
(insert-file-contents-literally file)
|
||
(let ((emojify-inhibit-emojify-in-current-buffer-p t))
|
||
(tar-mode))
|
||
(tar-untar-buffer))))
|
||
|
||
(defun emojify-download-emoji (emoji-set)
|
||
"Download the provided EMOJI-SET."
|
||
(interactive (list (completing-read "Select the emoji set you want to download: "
|
||
(ht-keys emojify-emoji-set-json))))
|
||
(let ((emoji-data (ht-get emojify-emoji-set-json emoji-set)))
|
||
(cond ((not emoji-data)
|
||
(error "No emoji set named %s found" emoji-set))
|
||
((and (file-exists-p (expand-file-name emoji-set emojify-emojis-dir))
|
||
(called-interactively-p 'any))
|
||
(message "%s emoji-set already downloaded, not downloading again!" emoji-set))
|
||
(t
|
||
(emojify--extract-emojis (emojify--emoji-download-emoji-set (ht-get emojify-emoji-set-json emoji-set)))))))
|
||
|
||
(defun emojify--confirm-emoji-download ()
|
||
"Confirm download of emojis.
|
||
|
||
This takes care of respecting the `emojify-download-emojis-p' and making sure we
|
||
do not prompt the user to download emojis multiple times."
|
||
(if (not (equal emojify-download-emojis-p 'ask))
|
||
emojify-download-emojis-p
|
||
;; Skip the prompt if we are in noninteractive mode or the user has already
|
||
;; denied us permission to download once
|
||
(unless (or noninteractive emojify--refused-image-download-p)
|
||
(let ((download-confirmed-p (yes-or-no-p "[emojify] Emoji images not available should I download them now?")))
|
||
(setq emojify--refused-image-download-p (not download-confirmed-p))
|
||
download-confirmed-p))))
|
||
|
||
(defun emojify-download-emoji-maybe ()
|
||
"Download emoji images if needed."
|
||
(when (and (equal emojify-display-style 'image)
|
||
(not (file-exists-p (emojify-image-dir)))
|
||
(not emojify--refused-image-download-p))
|
||
(unwind-protect
|
||
;; Do not prompt for download if download is in progress
|
||
(unless emojify--download-in-progress-p
|
||
(setq emojify--download-in-progress-p t)
|
||
(if (emojify--confirm-emoji-download)
|
||
(emojify-download-emoji emojify-emoji-set)
|
||
(warn "[emojify] Not downloading emoji images for now. Emojis would
|
||
not be displayed since images are not available. If you wish to download emojis,
|
||
run the command `emojify-download-emoji'")))
|
||
(setq emojify--download-in-progress-p nil))))
|
||
|
||
(defun emojify-ensure-images ()
|
||
"Ensure that emoji images are downloaded."
|
||
(if after-init-time
|
||
(emojify-download-emoji-maybe)
|
||
(add-hook 'after-init-hook #'emojify-download-emoji-maybe t)))
|
||
|
||
|
||
|
||
;; Minor mode definitions
|
||
|
||
(defun emojify-turn-on-emojify-mode ()
|
||
"Turn on `emojify-mode' in current buffer."
|
||
|
||
;; Calculate emoji data if needed
|
||
(emojify-create-emojify-emojis)
|
||
|
||
(when (emojify-buffer-p (current-buffer))
|
||
;; Download images if not available
|
||
(emojify-ensure-images)
|
||
|
||
;; Install our jit-lock function
|
||
(jit-lock-register #'emojify-redisplay-emojis-in-region)
|
||
(add-hook 'jit-lock-after-change-extend-region-functions #'emojify-after-change-extend-region-function t t)
|
||
|
||
;; Handle point entered behaviour
|
||
(add-hook 'post-command-hook #'emojify-detect-emoji-entry/exit t t)
|
||
|
||
;; Update emoji backgrounds after each command
|
||
(add-hook 'post-command-hook #'emojify-update-visible-emojis-background-after-command t t)
|
||
|
||
;; Update emoji backgrounds after mark is deactivated, this is needed since
|
||
;; deactivation can happen outside the command loop
|
||
(add-hook 'deactivate-mark-hook #'emojify-update-visible-emojis-background-after-command t t)
|
||
|
||
;; Update emoji backgrounds after when window scrolls
|
||
(add-hook 'window-scroll-functions #'emojify-update-visible-emojis-background-after-window-scroll t t)
|
||
|
||
;; Redisplay emojis after enabling `prettify-symbol-mode'
|
||
(add-hook 'prettify-symbols-mode-hook #'emojify-redisplay-emojis-in-region)
|
||
|
||
;; Redisplay visible emojis when emoji style changes
|
||
(add-hook 'emojify-emoji-style-change-hook #'emojify-redisplay-emojis-in-region)))
|
||
|
||
(defun emojify-turn-off-emojify-mode ()
|
||
"Turn off `emojify-mode' in current buffer."
|
||
;; Remove currently displayed emojis
|
||
(save-restriction
|
||
(widen)
|
||
(emojify-undisplay-emojis-in-region (point-min) (point-max)))
|
||
|
||
;; Uninstall our jit-lock function
|
||
(jit-lock-unregister #'emojify-redisplay-emojis-in-region)
|
||
(remove-hook 'jit-lock-after-change-extend-region-functions #'emojify-after-change-extend-region-function t)
|
||
|
||
(remove-hook 'post-command-hook #'emojify-detect-emoji-entry/exit t)
|
||
|
||
;; Disable hooks to update emoji backgrounds
|
||
(remove-hook 'post-command-hook #'emojify-update-visible-emojis-background-after-command t)
|
||
(remove-hook 'deactivate-mark-hook #'emojify-update-visible-emojis-background-after-command t)
|
||
(remove-hook 'window-scroll-functions #'emojify-update-visible-emojis-background-after-window-scroll t)
|
||
|
||
;; Remove hook to redisplay emojis after enabling `prettify-symbol-mode'
|
||
(remove-hook 'prettify-symbols-mode-hook #'emojify-redisplay-emojis-in-region)
|
||
|
||
;; Remove style change hooks
|
||
(remove-hook 'emojify-emoji-style-change-hook #'emojify-redisplay-emojis-in-region))
|
||
|
||
;;;###autoload
|
||
(define-minor-mode emojify-mode
|
||
"Emojify mode"
|
||
:init-value nil
|
||
(if emojify-mode
|
||
;; Turn on
|
||
(emojify-turn-on-emojify-mode)
|
||
;; Turn off
|
||
(emojify-turn-off-emojify-mode)))
|
||
|
||
;;;###autoload
|
||
(define-globalized-minor-mode global-emojify-mode
|
||
emojify-mode emojify-mode
|
||
:init-value nil)
|
||
|
||
(defadvice set-buffer-multibyte (after emojify-disable-for-unibyte-buffers (&rest ignored))
|
||
"Disable emojify when unibyte encoding is enabled for a buffer.
|
||
Re-enable it when buffer changes back to multibyte encoding."
|
||
(ignore-errors
|
||
(if enable-multibyte-characters
|
||
(when global-emojify-mode
|
||
(emojify-mode +1))
|
||
(emojify-mode -1))))
|
||
|
||
(ad-activate #'set-buffer-multibyte)
|
||
|
||
|
||
|
||
;; Displaying emojis in mode-line
|
||
|
||
(defun emojify--emojied-mode-line (format)
|
||
"Return an emojified version of mode-line FORMAT.
|
||
|
||
The format is converted to the actual string to be displayed using
|
||
`format-mode-line' and the unicode characters are replaced by images."
|
||
(if emojify-mode
|
||
;; Remove "%e" from format since we keep it as first part of the
|
||
;; emojified mode-line, see `emojify-emojify-mode-line'
|
||
(emojify-string (format-mode-line (delq "%e" format)) nil 'mode-line)
|
||
(format-mode-line format)))
|
||
|
||
(defun emojify-mode-line-emojified-p ()
|
||
"Check if the mode-line is already emojified.
|
||
|
||
If the `mode-line-format' is of following format
|
||
|
||
\(\"%e\" (:eval (emojify-emojied-mode-line ... )))
|
||
|
||
We can assume the mode-line is already emojified."
|
||
(and (consp mode-line-format)
|
||
(equal (ignore-errors (cl-caadr mode-line-format))
|
||
:eval)
|
||
(equal (ignore-errors (car (cl-cadadr mode-line-format)))
|
||
'emojify--emojied-mode-line)))
|
||
|
||
(defun emojify-emojify-mode-line ()
|
||
"Emojify unicode characters in the mode-line.
|
||
|
||
This updates `mode-line-format' to a modified version which emojifies the
|
||
mode-line before it is displayed."
|
||
(unless (emojify-mode-line-emojified-p)
|
||
(setq mode-line-format `("%e" (:eval
|
||
(emojify--emojied-mode-line ',mode-line-format))))))
|
||
|
||
(defun emojify-unemojify-mode-line ()
|
||
"Restore `mode-line-format' to unemojified version.
|
||
|
||
This basically reverses the effect of `emojify-emojify-mode-line'."
|
||
(when (emojify-mode-line-emojified-p)
|
||
(setq mode-line-format (cl-cadadr (cl-cadadr mode-line-format)))))
|
||
|
||
;;;###autoload
|
||
(define-minor-mode emojify-mode-line-mode
|
||
"Emojify mode line"
|
||
:init-value nil
|
||
(if emojify-mode-line-mode
|
||
;; Turn on
|
||
(emojify-emojify-mode-line)
|
||
;; Turn off
|
||
(emojify-unemojify-mode-line)))
|
||
|
||
;;;###autoload
|
||
(define-globalized-minor-mode global-emojify-mode-line-mode
|
||
emojify-mode-line-mode emojify-mode-line-mode
|
||
:init-value nil)
|
||
|
||
|
||
|
||
;; Searching emojis
|
||
|
||
(defvar emojify-apropos-buffer-name "*Apropos Emojis*")
|
||
|
||
(defun emojify-apropos-quit ()
|
||
"Delete the window displaying Emoji search results."
|
||
(interactive)
|
||
(if (= (length (window-list)) 1)
|
||
(bury-buffer)
|
||
(quit-window)))
|
||
|
||
(defun emojify-apropos-copy-emoji ()
|
||
"Copy the emoji being displayed at current line in apropos results."
|
||
(interactive)
|
||
(save-excursion
|
||
(goto-char (line-beginning-position))
|
||
(if (not (get-text-property (point) 'emojified))
|
||
(emojify-user-error "No emoji at point")
|
||
(kill-new (get-text-property (point) 'emojify-text))
|
||
(message "Copied emoji (%s) to kill ring!"
|
||
(get-text-property (point) 'emojify-text)))))
|
||
|
||
(defun emojify-apropos-describe-emoji ()
|
||
"Copy the emoji being displayed at current line in apropos results."
|
||
(interactive)
|
||
(save-excursion
|
||
(goto-char (line-beginning-position))
|
||
(if (not (get-text-property (point) 'emojified))
|
||
(emojify-user-error "No emoji at point")
|
||
(emojify-describe-emoji (get-text-property (point) 'emojify-text)))))
|
||
|
||
(defvar emojify-apropos-mode-map
|
||
(let ((map (make-sparse-keymap)))
|
||
(set-keymap-parent map emojify-common-mode-map)
|
||
(define-key map "c" #'emojify-apropos-copy-emoji)
|
||
(define-key map "w" #'emojify-apropos-copy-emoji)
|
||
(define-key map "d" #'emojify-apropos-describe-emoji)
|
||
(define-key map (kbd "RET") #'emojify-apropos-describe-emoji)
|
||
(define-key map "g" #'emojify-apropos-emoji)
|
||
map)
|
||
"Keymap used in `emojify-apropos-mode'.")
|
||
|
||
(define-derived-mode emojify-apropos-mode fundamental-mode "Apropos Emojis"
|
||
"Mode used to display results of `emojify-apropos-emoji'
|
||
|
||
\\{emojify-apropos-mode-map}"
|
||
(emojify-mode +1)
|
||
;; view mode being a minor mode eats up our bindings avoid it
|
||
(let (view-read-only)
|
||
(read-only-mode +1)))
|
||
|
||
(put 'emojify-apropos-mode 'mode-class 'special)
|
||
|
||
(defvar emojify--apropos-last-query nil)
|
||
(make-variable-buffer-local 'emojify--apropos-last-query)
|
||
|
||
(defun emojify-apropos-read-pattern ()
|
||
"Read apropos pattern with INITIAL-INPUT as the initial input.
|
||
|
||
Borrowed from apropos.el"
|
||
(let ((pattern (read-string (concat "Search for emoji (word list or regexp): ")
|
||
emojify--apropos-last-query)))
|
||
(if (string-equal (regexp-quote pattern) pattern)
|
||
(or (split-string pattern "[ \t]+" t)
|
||
(emojify-user-error "No word list given"))
|
||
pattern)))
|
||
|
||
;;;###autoload
|
||
(defun emojify-apropos-emoji (pattern)
|
||
"Show Emojis that match PATTERN."
|
||
(interactive (list (emojify-apropos-read-pattern)))
|
||
|
||
(emojify-create-emojify-emojis)
|
||
|
||
(let ((in-apropos-buffer-p (equal major-mode 'emojify-apropos-mode))
|
||
matching-emojis
|
||
sorted-emojis)
|
||
|
||
(unless (listp pattern)
|
||
(setq pattern (list pattern)))
|
||
|
||
;; Convert the user entered text to a regex to match the emoji name or
|
||
;; description
|
||
(apropos-parse-pattern pattern)
|
||
|
||
;; Collect matching emojis in a list of (list score emoji emoji-data)
|
||
;; elements, where score is the proximity of the emoji to given pattern
|
||
;; calculated using `apropos-score-str'
|
||
(emojify-emojis-each (lambda (key value)
|
||
(when (or (string-match apropos-regexp key)
|
||
(string-match apropos-regexp (ht-get value "name")))
|
||
(push (list (max (apropos-score-str key)
|
||
(apropos-score-str (ht-get value "name")))
|
||
key
|
||
value)
|
||
matching-emojis))))
|
||
|
||
;; Sort the emojis by the proximity score
|
||
(setq sorted-emojis (mapcar #'cdr
|
||
(sort matching-emojis
|
||
(lambda (emoji1 emoji2)
|
||
(> (car emoji1) (car emoji2))))))
|
||
|
||
;; Insert result in apropos buffer and display it
|
||
(with-current-buffer (get-buffer-create emojify-apropos-buffer-name)
|
||
(let ((inhibit-read-only t)
|
||
(query (mapconcat 'identity pattern " ")))
|
||
(erase-buffer)
|
||
(insert (propertize "Emojis matching" 'face 'apropos-symbol))
|
||
(insert (format " - \"%s\"" query))
|
||
(insert "\n\nUse `c' or `w' to copy emoji on current line\nUse `g' to rerun apropos\n\n")
|
||
(dolist (emoji sorted-emojis)
|
||
(insert (format "%s - %s (%s)"
|
||
(car emoji)
|
||
(ht-get (cadr emoji) "name")
|
||
(ht-get (cadr emoji) "style")))
|
||
(insert "\n"))
|
||
(goto-char (point-min))
|
||
(forward-line (1- 6))
|
||
(emojify-apropos-mode)
|
||
(setq emojify--apropos-last-query (concat query " "))
|
||
(setq-local line-spacing 7)))
|
||
|
||
(pop-to-buffer (get-buffer emojify-apropos-buffer-name)
|
||
(when in-apropos-buffer-p
|
||
(cons #'display-buffer-same-window nil)))))
|
||
|
||
|
||
|
||
;; Inserting emojis
|
||
|
||
(defun emojify--completing-read-minibuffer-setup-hook ()
|
||
"Enables `emojify-mode' in minbuffer while inserting emojis.
|
||
|
||
This ensures `emojify' is enabled even when `global-emojify-mode' is not on."
|
||
(emojify-mode +1))
|
||
|
||
(defun emojify--completing-read-helm-hook ()
|
||
"Enables `emojify-mode' in helm buffer.
|
||
|
||
This ensures `emojify' is enabled in helm buffer displaying completion even when
|
||
`global-emojify-mode' is not on."
|
||
(with-current-buffer helm-buffer
|
||
(emojify-mode +1)))
|
||
|
||
(defun emojify-completing-read (prompt &optional predicate require-match initial-input hist def inherit-input-method)
|
||
"Read emoji from the user and return the selected emoji.
|
||
|
||
PROMPT is a string to prompt with, PREDICATE, REQUIRE-MATCH, INITIAL-INPUT,
|
||
HIST, DEF, INHERIT-INPUT-METHOD correspond to the arguments for
|
||
`completing-read' and are passed to ‘completing-read’ without any
|
||
interpretation.
|
||
|
||
For each possible emoji PREDICATE is called with emoji text and data about the
|
||
emoji as a hash-table, the predate should return nil if it the emoji should
|
||
not be displayed for selection.
|
||
|
||
For example the following can be used to display only github style emojis for
|
||
selection
|
||
|
||
\(emojify-completing-read \"Select a Github style emoji: \"
|
||
(lambda (emoji data)
|
||
(equal (gethash \"style\" data) \"github\")))
|
||
|
||
This function sets up `ido', `icicles', `helm', `ivy' and vanilla Emacs
|
||
completion UI to display properly emojis."
|
||
(emojify-create-emojify-emojis)
|
||
(let* ((emojify-minibuffer-reading-emojis-p t)
|
||
(line-spacing 7)
|
||
(completion-ignore-case t)
|
||
(candidates (emojify--get-completing-read-candidates))
|
||
;; Vanilla Emacs completion and Icicles use the completion list mode to display candidates
|
||
;; the following makes sure emojify is enabled in the completion list
|
||
(completion-list-mode-hook (cons #'emojify--completing-read-minibuffer-setup-hook
|
||
completion-list-mode-hook))
|
||
;; (Vertical) Ido and Ivy displays candidates in minibuffer this makes sure candidates are emojified
|
||
;; when Ido or Ivy are used
|
||
(minibuffer-setup-hook (cons #'emojify--completing-read-minibuffer-setup-hook
|
||
minibuffer-setup-hook))
|
||
(helm-after-initialize-hook (cons #'emojify--completing-read-helm-hook
|
||
(bound-and-true-p helm-after-initialize-hook))))
|
||
(car (split-string (completing-read prompt
|
||
candidates
|
||
predicate
|
||
require-match
|
||
initial-input
|
||
hist
|
||
def
|
||
inherit-input-method)
|
||
" "))))
|
||
|
||
;;;###autoload
|
||
(defun emojify-insert-emoji ()
|
||
"Interactively prompt for Emojis and insert them in the current buffer.
|
||
|
||
This respects the `emojify-emoji-styles' variable."
|
||
(interactive)
|
||
(insert (emojify-completing-read "Insert Emoji: ")))
|
||
|
||
|
||
|
||
;; Describing emojis
|
||
|
||
(defvar emojify-help-buffer-name "*Emoji Help*")
|
||
|
||
(defvar-local emojify-described-emoji nil)
|
||
|
||
(defun emojify-description-copy-emoji ()
|
||
"Copy the emoji being displayed at current line in apropos results."
|
||
(interactive)
|
||
(save-excursion
|
||
(kill-new emojify-described-emoji)
|
||
(message "Copied emoji (%s) to kill ring!" emojify-described-emoji)))
|
||
|
||
(defvar emojify-description-mode-map
|
||
(let ((map (make-sparse-keymap)))
|
||
(set-keymap-parent map emojify-common-mode-map)
|
||
(define-key map "c" #'emojify-description-copy-emoji)
|
||
(define-key map "w" #'emojify-description-copy-emoji)
|
||
map)
|
||
"Keymap used in `emojify-description-mode'.")
|
||
|
||
(define-derived-mode emojify-description-mode fundamental-mode "Describe Emoji"
|
||
"Mode used to display results of description for emojis.
|
||
|
||
\\{emojify-description-mode-map}"
|
||
(emojify-mode +1)
|
||
;; view mode being a minor mode eats up our bindings avoid it
|
||
(let (view-read-only)
|
||
(read-only-mode +1))
|
||
(goto-address-mode +1))
|
||
|
||
(put 'emojify-description-mode 'mode-class 'special)
|
||
|
||
(defun emojify--display-emoji-description-buffer (emoji)
|
||
"Display description for EMOJI."
|
||
(with-current-buffer (get-buffer-create emojify-help-buffer-name)
|
||
(let ((inhibit-read-only t))
|
||
(erase-buffer)
|
||
(save-excursion
|
||
(insert (propertize (ht-get emoji "emoji") 'emojify-inhibit t)
|
||
" - Displayed as "
|
||
(propertize (ht-get emoji "emoji") 'emojify-force-display t)
|
||
"\n\n")
|
||
(insert (propertize "Name" 'face 'font-lock-keyword-face)
|
||
": "
|
||
(ht-get emoji "name") "\n")
|
||
(insert (propertize "Style" 'face 'font-lock-keyword-face)
|
||
": "
|
||
(ht-get emoji "style") "\n")
|
||
(insert (propertize "Image used" 'face 'font-lock-keyword-face)
|
||
": "
|
||
(expand-file-name (ht-get emoji "image")
|
||
(emojify-image-dir))
|
||
"\n")
|
||
(when (and (not (string= (ht-get emoji "style") "unicode"))
|
||
(ht-get emoji "unicode"))
|
||
(insert (propertize "Unicode representation"
|
||
'face 'font-lock-keyword-face)
|
||
": "
|
||
(propertize (ht-get emoji "unicode") 'emojify-inhibit t)
|
||
"\n"))
|
||
(when (and (not (string= (ht-get emoji "style") "ascii"))
|
||
(ht-get emoji "ascii"))
|
||
(insert (propertize "Ascii representation"
|
||
'face 'font-lock-keyword-face)
|
||
": "
|
||
(propertize (ht-get emoji "ascii") 'emojify-inhibit t)
|
||
"\n"))
|
||
(insert (propertize "User defined"
|
||
'face 'font-lock-keyword-face)
|
||
": "
|
||
(if (ht-get emoji "custom") "Yes" "No")
|
||
"\n")
|
||
(unless (ht-get emoji "custom")
|
||
(when (or (ht-get emoji "unicode")
|
||
(string= (ht-get emoji "style") "unicode"))
|
||
(insert (propertize "Unicode Consortium" 'face 'font-lock-keyword-face)
|
||
": "
|
||
(concat "http://www.unicode.org/emoji/charts-beta/full-emoji-list.html#"
|
||
(string-join (mapcar (apply-partially #'format "%x")
|
||
(string-to-list (or (ht-get emoji "unicode")
|
||
(ht-get emoji "emoji"))))
|
||
"_"))
|
||
"\n"))
|
||
(insert (propertize "Emojipedia" 'face 'font-lock-keyword-face)
|
||
": "
|
||
(let* ((tone-stripped (replace-regexp-in-string "- *[Tt]one *\\([0-9]+\\)$"
|
||
"- type \\1"
|
||
(ht-get emoji "name")))
|
||
(non-alphanumeric-stripped (replace-regexp-in-string "[^0-9a-zA-Z]"
|
||
" "
|
||
tone-stripped))
|
||
(words (split-string non-alphanumeric-stripped " " t " ")))
|
||
(concat "http://emojipedia.org/"
|
||
(downcase (emojify--string-join words "-"))))
|
||
"\n"))))
|
||
(emojify-description-mode)
|
||
(setq emojify-described-emoji (ht-get emoji "emoji")))
|
||
(display-buffer (get-buffer emojify-help-buffer-name))
|
||
(get-buffer emojify-help-buffer-name))
|
||
|
||
(defun emojify-describe-emoji (emoji-text)
|
||
"Display description for EMOJI-TEXT."
|
||
(interactive (list (emojify-completing-read "Describe Emoji: ")))
|
||
(if (emojify-get-emoji emoji-text)
|
||
(emojify--display-emoji-description-buffer (emojify-get-emoji emoji-text))
|
||
(emojify-user-error "No emoji found for '%s'" emoji-text)))
|
||
|
||
(defun emojify-describe-emoji-at-point ()
|
||
"Display help for EMOJI displayed at point."
|
||
(interactive)
|
||
(if (not (get-text-property (point) 'emojified))
|
||
(emojify-user-error "No emoji at point!")
|
||
(emojify--display-emoji-description-buffer (emojify-get-emoji (get-text-property (point) 'emojify-text)))))
|
||
|
||
|
||
|
||
;; Listing emojis
|
||
|
||
(defun emojify-list-copy-emoji ()
|
||
"Copy the emoji being displayed at current line in apropos results."
|
||
(interactive)
|
||
(save-excursion
|
||
(let ((emoji (get-text-property (point) 'tabulated-list-id)))
|
||
(if (not emoji)
|
||
(emojify-user-error "No emoji at point")
|
||
(kill-new emoji)
|
||
(message "Copied emoji (%s) to kill ring!" emoji)))))
|
||
|
||
(defun emojify-list-describe-emoji ()
|
||
"Copy the emoji being displayed at current line in apropos results."
|
||
(interactive)
|
||
(save-excursion
|
||
(let ((emoji (get-text-property (point) 'tabulated-list-id)))
|
||
(if (not emoji)
|
||
(emojify-user-error "No emoji at point")
|
||
(emojify-describe-emoji emoji)))))
|
||
|
||
(defvar-local emojify-list--emojis-displayed nil
|
||
"Record that emojis have been successfully displayed in the current buffer.
|
||
|
||
`emojify-list-emojis' checks to this decide if it should print the entries
|
||
again.")
|
||
|
||
(defun emojify-list-force-refresh ()
|
||
"Force emoji list to be refreshed."
|
||
(interactive)
|
||
(setq emojify-list--emojis-displayed nil)
|
||
(emojify-list-emojis))
|
||
|
||
(defvar emojify-list-mode-map
|
||
(let ((map (make-sparse-keymap)))
|
||
(set-keymap-parent map emojify-common-mode-map)
|
||
(define-key map "c" #'emojify-list-copy-emoji)
|
||
(define-key map "w" #'emojify-list-copy-emoji)
|
||
(define-key map "d" #'emojify-list-describe-emoji)
|
||
(define-key map "g" #'emojify-list-force-refresh)
|
||
(define-key map (kbd "RET") #'emojify-list-describe-emoji)
|
||
map)
|
||
"Keymap used in `emojify-list-mode'.")
|
||
|
||
(defun emojify-list-printer (id cols)
|
||
"Printer used to print the emoji rows in tabulated list.
|
||
|
||
See `tabulated-list-print-entry' to understand the arguments ID and COLS."
|
||
(let ((beg (point))
|
||
(padding (max tabulated-list-padding 0))
|
||
(inhibit-read-only t))
|
||
(when (> tabulated-list-padding 0)
|
||
(insert (make-string padding ?\s)))
|
||
|
||
(tabulated-list-print-col 0
|
||
(propertize (aref cols 0) 'emojify-inhibit t)
|
||
(current-column))
|
||
|
||
;; Inhibit display of second column ("Text") as emoji
|
||
(tabulated-list-print-col 1
|
||
(propertize (aref cols 1) 'emojify-inhibit t)
|
||
(current-column))
|
||
|
||
;; The type of this emoji
|
||
(tabulated-list-print-col 2
|
||
(aref cols 2)
|
||
(current-column))
|
||
|
||
;; Is this a custom emoji
|
||
(tabulated-list-print-col 3
|
||
(aref cols 3)
|
||
(current-column))
|
||
|
||
;; Force display of last column ("Display") as emoji
|
||
(tabulated-list-print-col 4
|
||
(propertize (aref cols 4) 'emojify-force-display t)
|
||
(current-column))
|
||
|
||
(insert ?\n)
|
||
(add-text-properties beg
|
||
(point)
|
||
`(tabulated-list-id ,id tabulated-list-entry ,cols))
|
||
|
||
(message "Listing emojis (%d of %d) ..." (1- (line-number-at-pos)) (aref cols 5))))
|
||
|
||
(defun emojify-list-entries ()
|
||
"Return entries to display in tabulated list."
|
||
(emojify-create-emojify-emojis)
|
||
|
||
(let (entries count)
|
||
(emojify-emojis-each (lambda (emoji data)
|
||
(push (list emoji (vector (ht-get data "name")
|
||
emoji
|
||
(ht-get data "style")
|
||
(if (ht-get data "custom") "Yes" "No")
|
||
emoji))
|
||
entries)))
|
||
|
||
(setq count (length entries))
|
||
|
||
(mapcar (lambda (entry)
|
||
(list (car entry) (vconcat (cadr entry) (vector count))))
|
||
entries)))
|
||
|
||
(define-derived-mode emojify-list-mode tabulated-list-mode "Emoji-List"
|
||
"Major mode for listing emojis.
|
||
\\{emojify-list-mode-map}"
|
||
(setq line-spacing 7
|
||
tabulated-list-format [("Name" 30 t)
|
||
("Text" 20 t)
|
||
("Style" 10 t)
|
||
("Custom" 10 t)
|
||
("Display" 20 nil)]
|
||
tabulated-list-sort-key (cons "Name" nil)
|
||
tabulated-list-padding 2
|
||
tabulated-list-entries #'emojify-list-entries
|
||
tabulated-list-printer #'emojify-list-printer)
|
||
(tabulated-list-init-header))
|
||
|
||
(defun emojify-list-emojis ()
|
||
"List emojis in a tabulated view."
|
||
(interactive)
|
||
(let ((buffer (get-buffer-create "*Emojis*")))
|
||
(with-current-buffer buffer
|
||
(unless emojify-list--emojis-displayed
|
||
(emojify-list-mode)
|
||
(tabulated-list-print)
|
||
(setq emojify-list--emojis-displayed t))
|
||
(pop-to-buffer buffer))))
|
||
|
||
|
||
|
||
;; Integration with company mode
|
||
|
||
(defadvice company-pseudo-tooltip-unhide (after emojify-display-emojis-in-company-tooltip (&rest ignored))
|
||
"Advice to display emojis in company mode tooltips.
|
||
|
||
This function does two things, first it adds text properties to the completion
|
||
tooltip to make it display the emojis, secondly it makes company always render
|
||
the completion tooltip using `after-string' overlay property rather than
|
||
`display' text property.
|
||
|
||
The second step is needed because emojify displays the emojis using `display'
|
||
text property, similarly company-mode in some cases uses `display' overlay
|
||
property to render its pop, this results into a `display' property inside
|
||
`display' property, however Emacs ignores recursive display text property.
|
||
Using `after-string' works as well as `display' while allowing the emojis to be
|
||
displayed."
|
||
(when (and emojify-mode
|
||
emojify-company-tooltips-p
|
||
(overlayp (bound-and-true-p company-pseudo-tooltip-overlay)))
|
||
(let* ((ov company-pseudo-tooltip-overlay)
|
||
(disp (or (overlay-get ov 'display)
|
||
(overlay-get ov 'after-string)))
|
||
(emojified-display (when disp
|
||
(emojify-string disp)))
|
||
(emojified-p (when emojified-display
|
||
(text-property-any 0 (1- (length emojified-display))
|
||
'emojified t
|
||
emojified-display))))
|
||
;; Do not switch to after-string if menu is not emojified
|
||
(when (and disp emojified-p)
|
||
(overlay-put ov 'after-string nil)
|
||
(overlay-put ov 'display (propertize " " 'invisible t))
|
||
(overlay-put ov 'after-string emojified-display)))))
|
||
|
||
(ad-activate #'company-pseudo-tooltip-unhide)
|
||
|
||
|
||
|
||
;; Integration with some miscellaneous functionality
|
||
|
||
(defadvice mouse--drag-set-mark-and-point (after emojify-update-emoji-background (&rest ignored))
|
||
"Advice to update emoji backgrounds after selection is changed using mouse.
|
||
|
||
Currently there are no hooks run after mouse movements, as such the emoji
|
||
backgrounds are updated only after the mouse button is released. This advices
|
||
`mouse--drag-set-mark-and-point' which is run after selection changes to trigger
|
||
an update of emoji backgrounds. Not the cleanest but the only way I can think of."
|
||
(when emojify-mode
|
||
(emojify-update-visible-emojis-background-after-command)))
|
||
|
||
(ad-activate #'mouse--drag-set-mark-and-point)
|
||
|
||
(defadvice text-scale-increase (after emojify-resize-emojis (&rest ignored))
|
||
"Advice `text-scale-increase' to resize emojis on text resize."
|
||
(when emojify-mode
|
||
(let ((new-font-height (emojify-default-font-height)))
|
||
(emojify-do-for-emojis-in-region (point-min) (point-max)
|
||
(plist-put (cdr (get-text-property emoji-start 'display))
|
||
:height
|
||
new-font-height)))))
|
||
|
||
(ad-activate #'text-scale-increase)
|
||
|
||
|
||
|
||
(provide 'emojify)
|
||
;;; emojify.el ends here
|