;;; evil-cleverparens-util.el --- Utility functions for evil-cleverparens ;; ;; Copyright (C) 2015 Olli Piepponen ;; ;; Author: Olli Piepponen ;; URL: https://github.com/luxbock/evil-cleverparens ;; Keywords: cleverparens, parentheses, evil, paredit, smartparens ;; Version: 0.1.0 ;; Package-Requires: ((evil "1.0") (smartparens "1.6.1")) ;; ;; This file is NOT part of GNU Emacs. ;; ;; This file is free software (MIT License) ;;; Commentary: ;; Helpers and utility functions for evil-cleverparens. ;;; Code: (require 'cl-lib) (require 'dash) (require 'evil) (require 'paredit) (require 'smartparens) (require 'subr-x) (defvar evil-cp-pair-list '(("(" . ")") ("[" . "]") ("{" . "}") ("\"" . "\"")) "List of parentheses pairs recognized by evil-cleverparens.") (defvar evil-cp-sp-pair-list (-filter (lambda (pair) (not (string-equal (car pair) "`"))) sp-pair-list)) (defvar evil-cp--ws-regexp "[ \n\r\t]") (defun evil-cp--looking-at-string-delimiter-p () "Predicate for checking if the point is on a string delimiter." (and (looking-at (sp--get-stringlike-regexp)) (not (paredit-in-string-escape-p)))) (defun evil-cp--looking-at-whitespace-p (&optional pos) (save-excursion (when pos (goto-char pos)) (looking-at-p evil-cp--ws-regexp))) (defun evil-cp--pair-for (pair pairs) (cond ((not pairs) (message "Pair for %s not found." pair)) ((string= pair (caar pairs)) (car pairs)) (t (evil-cp--pair-for pair (cdr pairs))))) (defun evil-cp-pair-for (pair) (evil-cp--pair-for pair evil-cp-pair-list)) (defun evil-cp--get-opening-regexp () (sp--strict-regexp-opt (--map (car it) evil-cp-pair-list))) (defun evil-cp--get-closing-regexp () (sp--strict-regexp-opt (--map (cdr it) evil-cp-pair-list))) (defun evil-cp--looking-at-opening-p (&optional pos) "Predicate that returns true if point is looking at an opening parentheses as defined by smartparens for the major mode in question. Ignores parentheses inside strings." (save-excursion (when pos (goto-char pos)) (and (sp--looking-at-p (evil-cp--get-opening-regexp)) (not (evil-cp--inside-string-p))))) (defun evil-cp--looking-at-closing-p (&optional pos) "Predicate that returns true if point is looking at an closing paren as defined by smartparens for the major mode in question. Ignores parentheses inside strings." (save-excursion (when pos (goto-char pos)) (and (sp--looking-at-p (evil-cp--get-closing-regexp)) (not (evil-cp--inside-string-p))))) (defun evil-cp--looking-at-paren-p (&optional pos) "Predicate that returns true if point is looking at a parentheses as defined by smartparens for the major mode in question. Ignores parentheses inside strings." (save-excursion (when pos (goto-char pos)) (and (sp--looking-at-p (sp--get-allowed-regexp)) (not (evil-cp--inside-string-p))))) (defun evil-cp--looking-at-any-delimiter (&optional pos) "Predicate that returns true if point is on top of a parentheses or a string delimiter as defined by smartparens for the major mode in question." (save-excursion (when pos (goto-char pos)) (or (sp--looking-at-p (sp--get-stringlike-regexp)) (evil-cp--looking-at-paren-p)))) (defun evil-cp--looking-at-string-opening-p (&optional pos) "Predicate for checking if point is on a opening string delimiter." (save-excursion (when pos (goto-char pos)) (and (evil-cp--looking-at-string-delimiter-p) (progn (forward-char) (nth 3 (syntax-ppss)))))) (defun evil-cp--looking-at-string-closing-p (&optional pos) "Predicate for checking if point is on a closing delimiter." (save-excursion (when pos (goto-char pos)) (and (evil-cp--looking-at-string-delimiter-p) (not (paredit-in-string-escape-p)) (nth 3 (syntax-ppss))))) (defun evil-cp--looking-at-any-opening-p (&optional pos) "Predicate to check if point (or POS) is on an opening parentheses or a string delimiter." (or (evil-cp--looking-at-opening-p pos) (evil-cp--looking-at-string-opening-p pos))) (defun evil-cp--looking-at-any-closing-p (&optional pos) "Predicate to check if point (or POS) is on an opening parentheses or a string delimiter." (or (evil-cp--looking-at-closing-p pos) (evil-cp--looking-at-string-closing-p pos))) (defun evil-cp-region-ok-p (beg end) "The same as `sp-region-ok-p' where `' are not treated as a valid pair." (let ((sp-pair-list evil-cp-sp-pair-list)) (sp-region-ok-p beg end))) (defmacro evil-cp--guard-point (&rest body) "Evil/Vim and Emacs have different opinions on where the point is with respect to the visible cursor. This macro is used to make sure that commands that are used to the Emacs view still work when the cursor in evil is on top of an opening parentheses or a string delimiter." `(if (evil-cp--looking-at-any-opening-p) (save-excursion (forward-char 1) ,@body) ,@body)) (defmacro evil-cp--point-after (&rest body) "Return location of point after performing body." `(save-excursion ,@body (point))) (defmacro evil-cp--movement-bounds (movement) "Returns a cons-pair containing the range of motion that executing MOVEMENT performs or nil if point wasn't moved." (let ((beg (cl-gensym)) (end (cl-gensym))) `(let ((,beg (point)) (,end (evil-cp--point-after ,movement))) (when (not (= ,beg ,end)) (cons ,beg ,end))))) (defmacro evil-cp--safe-line-bounds (&rest body) "Performs the actions in BODY and checks if the line where point lands is a safe region, in which case its bounds are returned." `(save-excursion ,@body (let ((pbol (point-at-bol)) (peol (point-at-eol))) (when (and (sp-region-ok-p pbol peol)) (cons pbol peol))))) (defmacro evil-cp--next-thing-bounds (&optional movement by-line-p) "Fetches the bounds for the next hing which may be a symbol, a form, a line or a comment block." `(save-excursion ,movement (evil-cp--next-thing-bounds* ,by-line-p))) (defmacro evil-cp--previous-thing-bounds (&optional movement by-line-p) "Fetches the bounds for the previous thing which may be a symbol, a form, a line or a comment block." `(save-excursion ,movement (evil-cp--previous-thing-bounds* ,by-line-p))) (defun evil-cp--looking-at-empty-form () "A predicate for checking if the point is currently looking at an empty form." (evil-cp--guard-point (or (sp-point-in-empty-sexp) (sp-point-in-empty-string)))) (defun evil-cp--inside-form-p (&optional pos) "Predicate for checking if point is inside a sexp." (save-excursion (when pos (goto-char pos)) (when (evil-cp--looking-at-opening-p) (forward-char)) (not (zerop (nth 0 (syntax-ppss)))))) (defun evil-cp--inside-string-p (&optional pos) "Predicate that returns true if point is inside a string." (save-excursion (when pos (goto-char pos)) (when (not (or (eobp) (bobp))) (let ((string-ppss (nth 3 (syntax-ppss)))) (or string-ppss (progn (forward-char) (nth 3 (syntax-ppss)))))))) (defun evil-cp--inside-any-form-p (&optional pos) "Predicate that returns true if point is either inside a sexp or a string." (or (evil-cp--inside-form-p pos) (evil-cp--inside-string-p pos))) (defun evil-cp--top-level-form-p (&optional pos) "Predicate that returns true if point is inside a top-level sexp." (save-excursion (when pos (goto-char pos)) (evil-cp--guard-point (let* ((ppss (syntax-ppss)) (n0 (nth 0 ppss)) (n3 (nth 3 ppss))) (or (and (eq n0 1) (not n3)) ; top-level sexp (and (eq n0 0) n3)))))) ; top-level string (defun evil-cp--outside-form-p (&optional pos) "Prediacate for checking if the point is outside any form or string." (let ((sppss (syntax-ppss pos))) (and (zerop (car sppss)) (not (nth 3 sppss))))) (defun evil-cp--string-bounds (&optional pos) "Returns the location of the beginning and ending positions for a string form. Accepts an optional argument POS for moving the point to that location." (let ((pos (or pos (point)))) (save-excursion (goto-char pos) (or (sp-get-quoted-string-bounds) (progn (forward-char) (sp-get-quoted-string-bounds)))))) (defun evil-cp--point-in-comment (&optional pos) "Slightly cheaper to check version of `sp-point-in-comment'. Does not support comments with open/closing delimiters." (setq pos (or pos (point))) (or (looking-at "\\s<") (save-excursion (nth 4 (syntax-ppss pos))))) (defun evil-cp--point-in-string-or-comment (&optional pos) "Slightly cheaper to check version of `sp-point-in-string-or-comment'. Does not support comments with open/closing delimiters." (or (evil-cp--point-in-comment pos) (sp-point-in-string pos))) (defun evil-cp--skip-whitespace-and-comments (&optional reversep) "Skips whitespace and comments forward." (catch 'stop (if reversep (while (or (looking-back evil-cp--ws-regexp) (evil-cp--point-in-comment (1- (point)))) (backward-char) (when (bobp) (throw 'stop :bobp))) (while (or (looking-at evil-cp--ws-regexp) (evil-cp--point-in-comment)) (forward-char) (when (looking-at sp-comment-char) (forward-line)) (when (eobp) (throw 'stop :eobp)))))) (defun evil-cp--backward-up-list (&optional ignore-strings-p) "Workaround for `backward-up-list' not working inside strings. If IGNORE-STRINGS-P is t then strings are ignored when moving up. Otherwise they are treated as lists. Returns the location of point when the operation was successful." (interactive) (when (let ((sppss (syntax-ppss))) (or (not (zerop (car sppss))) (nth 3 sppss))) (if (not (in-string-p)) (progn (backward-up-list) (point)) (when (not (and ignore-strings-p (evil-cp--top-level-form-p))) (while (in-string-p) (backward-char)) (when ignore-strings-p (backward-up-list)) (point))))) (defun evil-cp--up-list (&optional ignore-strings-p) "Workaround for `up-list' not working inside strings. If IGNORE-STRINGS-P is t then strings are ignored when moving up. Otherwise they are treated as lists. Returns the location of point when the operation was successful." (interactive) (when (let ((sppss (syntax-ppss))) (or (not (zerop (car sppss))) (nth 3 sppss))) (if (not (in-string-p)) (progn (up-list) (point)) (when (not (and ignore-strings-p (evil-cp--top-level-form-p))) (while (in-string-p) (forward-char)) (when ignore-strings-p (up-list)) (point))))) (defun evil-cp--top-level-bounds (&optional pos) "Returns the bounds for the top-level form point is in, or POS if given. Note that this is different from defun-bounds, as defun would ignore top-level forms that are on the same line." (save-excursion (when pos (goto-char pos)) (when (evil-cp--inside-form-p) (catch 'done (while t (evil-cp--backward-up-list) (when (evil-cp--top-level-form-p) (throw 'done (cons (point) (forward-list))))))))) (defun evil-cp--beginning-of-top-level () (when (evil-cp--inside-form-p) (goto-char (car (evil-cp--top-level-bounds))))) (defun evil-cp--end-of-top-level () (when (evil-cp--inside-form-p) (goto-char (cdr (evil-cp--top-level-bounds))))) (defun evil-cp--matching-paren-pos (&optional pos) "Finds the location of the matching paren for where point is located. Also works for strings. Takes an optional POS argument for temporarily moving the point to before proceeding. Assumes that the parentheses in the buffer are balanced." (save-excursion (cond ((evil-cp--looking-at-opening-p) (progn (sp-forward-sexp 1) (backward-char) (point))) ((evil-cp--looking-at-closing-p) (progn (forward-char 1) (sp-backward-sexp 1) (point))) ((looking-at "\"") (let* ((bounds (evil-cp--string-bounds))) (if (= (car bounds) (point)) (1- (cdr bounds)) (car bounds))))))) (defun evil-cp--text-balanced-p (text) "Checks if the string TEXT is balanced or not." (with-temp-buffer (insert text) (sp-region-ok-p (point-min) (point-max)))) (defun evil-cp--balanced-block-p (beg end) "Checks whether the block defined by BEG and END contains balanced parentheses." (let ((region (evil-yank-rectangle beg end))) (with-temp-buffer (insert region) (sp-region-ok-p (point-min) (point-max))))) (defun evil-cp--sp-obj-bounds (thing) "Helper for turning a smartparens text object into a cons-pair." (sp-get thing (when :beg (cons :beg :end)))) (defun evil-cp--get-enclosing-bounds (&optional prefixp) "Returns a tuple of start/end positions for the surrounding sexp. If PREFIXP is true, then the beginning bound starts from the beginning of the prefix as defined by `sp-sexp-prefix'." (when (evil-cp--inside-any-form-p) (save-excursion (evil-cp--guard-point (sp-get (if (sp-point-in-string (point)) (sp-get-string t) (sp-get-enclosing-sexp)) (cons (if (and prefixp (not (string-empty-p :prefix))) (- :beg (length :prefix)) :beg) :end)))))) (defun evil-cp--next-sexp-bounds (&optional pos) (save-excursion (when pos (goto-char pos)) (when (not (evil-cp--looking-at-whitespace-p)) (evil-cp--movement-bounds (forward-sexp))))) (provide 'evil-cleverparens-util) ;;; evil-cleverparens-util.el ends here