mwim.el 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. ;;; mwim.el --- Switch between the beginning/end of line or code -*- lexical-binding: t -*-
  2. ;; Copyright © 2015, 2016, 2018 Alex Kost
  3. ;; Author: Alex Kost <alezost@gmail.com>
  4. ;; Created: 9 Jan 2015
  5. ;; Version: 0.4
  6. ;; URL: https://github.com/alezost/mwim.el
  7. ;; Keywords: convenience
  8. ;; This program is free software; you can redistribute it and/or modify
  9. ;; it under the terms of the GNU General Public License as published by
  10. ;; the Free Software Foundation, either version 3 of the License, or
  11. ;; (at your option) any later version.
  12. ;;
  13. ;; This program is distributed in the hope that it will be useful,
  14. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. ;; GNU General Public License for more details.
  17. ;;
  18. ;; You should have received a copy of the GNU General Public License
  19. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. ;;; Commentary:
  21. ;; MWIM stands for "Move Where I Mean". This package is inspired by
  22. ;; <http://www.emacswiki.org/emacs/BackToIndentationOrBeginning>. It
  23. ;; provides commands to switch between various positions on the current
  24. ;; line (particularly, to move to the beginning/end of code, line or
  25. ;; comment).
  26. ;; To install the package manually, add the following to your init file:
  27. ;;
  28. ;; (add-to-list 'load-path "/path/to/mwim-dir")
  29. ;; (autoload 'mwim "mwim" nil t)
  30. ;; (autoload 'mwim-beginning "mwim" nil t)
  31. ;; (autoload 'mwim-end "mwim" nil t)
  32. ;; (autoload 'mwim-beginning-of-code-or-line "mwim" nil t)
  33. ;; (autoload 'mwim-beginning-of-line-or-code "mwim" nil t)
  34. ;; (autoload 'mwim-beginning-of-code-or-line-or-comment "mwim" nil t)
  35. ;; (autoload 'mwim-end-of-code-or-line "mwim" nil t)
  36. ;; (autoload 'mwim-end-of-line-or-code "mwim" nil t)
  37. ;; Then you can bind some keys to some of those commands and start
  38. ;; moving. See README in the source repo for more details.
  39. ;;; Code:
  40. (defgroup mwim nil
  41. "Move Where I Mean.
  42. Move the point to various line positions."
  43. :group 'convenience)
  44. (defcustom mwim-beginning-of-line-function
  45. '((t . beginning-of-line)
  46. (message-mode . message-beginning-of-line)
  47. (org-mode . org-beginning-of-line))
  48. "Function(s) used to move the point to the beginning of line.
  49. Can either be a function or an alist of the following form:
  50. ((MODE-NAME . FUNCTION) ...)
  51. where MODE-NAME is either a `major-mode' name or `t' (used as a
  52. default value for any unspecified mode), and FUNCTION is the
  53. moving function for this mode."
  54. :type '(choice (function-item beginning-of-visual-line)
  55. (function-item beginning-of-line)
  56. (alist :key-type symbol :value-type function)
  57. (function :tag "Another function"))
  58. :group 'mwim)
  59. (defcustom mwim-end-of-line-function
  60. '((t . end-of-line)
  61. (org-mode . org-end-of-line))
  62. "Function(s) used to move the point to the end of line.
  63. See also `mwim-beginning-of-line-function'."
  64. :type '(choice (function-item end-of-visual-line)
  65. (function-item end-of-line)
  66. (alist :key-type symbol :value-type function)
  67. (function :tag "Another function"))
  68. :group 'mwim)
  69. (defcustom mwim-next-position-function nil
  70. "Function used to define the next position.
  71. This function is called with a list of functions returning
  72. available point positions as a single argument. It should return
  73. the next position where the point will be moved.
  74. There are 2 functions to choose from: `mwim-next-position' and
  75. `mwim-next-unique-position'.
  76. `mwim-next-position' is faster as it calculates positions only
  77. when needed, however there are some special cases when this
  78. function will not return an expected position (when there are
  79. more than 3 potential positions, and some of them are the same).
  80. With `mwim-next-unique-position' you will always switch between
  81. all available positions, but it is slower as it calculates all
  82. positions at once. Most likely, however, this slowness will be
  83. insignificant as the number of potential positions is not big.
  84. If this variable is nil, an appropriate function will be chosen
  85. automatically. This is the recommended value, as it provides the
  86. speed when possible, and guaranteed cycling between all positions
  87. for complex cases."
  88. :type '(choice (const nil :tag "Choose automatically")
  89. (function-item mwim-next-position)
  90. (function-item mwim-next-unique-position)
  91. (function :tag "Another function"))
  92. :group 'mwim)
  93. (defcustom mwim-beginning-position-functions
  94. '(mwim-block-beginning
  95. mwim-code-beginning
  96. mwim-line-beginning
  97. mwim-comment-beginning)
  98. "List of functions used by `\\[mwim-beginning]' command."
  99. :type '(repeat function)
  100. :group 'mwim)
  101. (defcustom mwim-end-position-functions
  102. '(mwim-block-end
  103. mwim-code-end
  104. mwim-line-end)
  105. "List of functions used by `\\[mwim-end]' command."
  106. :type '(repeat function)
  107. :group 'mwim)
  108. (defcustom mwim-position-functions
  109. '(mwim-line-beginning
  110. mwim-code-beginning
  111. mwim-comment-beginning
  112. mwim-code-end
  113. mwim-line-end)
  114. "List of functions used by `\\[mwim]' command."
  115. :type '(repeat function)
  116. :group 'mwim)
  117. (defun mwim-function (fun-or-alist)
  118. "Return function depending on FUN-OR-ALIST and the current `major-mode'.
  119. FUN-OR-ALIST should have the same form as
  120. `mwim-beginning-of-line-function' variable."
  121. (cond
  122. ((functionp fun-or-alist)
  123. fun-or-alist)
  124. ((listp fun-or-alist)
  125. (or (cdr (assq major-mode fun-or-alist))
  126. (cdr (assq t fun-or-alist))))))
  127. ;;; Calculating positions
  128. (defmacro mwim-point-at (&rest body)
  129. "Return point position after evaluating BODY in `save-excursion'."
  130. (declare (debug t) (indent 0))
  131. `(save-excursion ,@body (point)))
  132. (defun mwim-first-position (functions &optional position)
  133. "Return the first point position that is not POSITION from
  134. positions defined after calling FUNCTIONS.
  135. If POSITION is nil, use the current point position.
  136. Initially, the first function is called (without arguments). If
  137. the resulting position is not the same as POSITION, return it.
  138. Otherwise, call the second function, etc.
  139. If after calling all FUNCTIONS, all resulting positions are
  140. the same as POSITION, return nil."
  141. (or position (setq position (point)))
  142. (pcase functions
  143. (`(,first . ,rest)
  144. (let ((pos (funcall first)))
  145. (if (and pos (/= pos position))
  146. pos
  147. (mwim-first-position rest position))))))
  148. (defun mwim-next-position (functions &optional position fallback-position)
  149. "Return the next point position after POSITION from positions
  150. defined after calling FUNCTIONS.
  151. If POSITION is nil, use the current point position.
  152. Initially, the first function is called (without arguments). If
  153. the resulting position is the same as POSITION, return position
  154. defined after calling the second function. If it is not the
  155. same, compare the second position with POSITION, etc.
  156. If after calling all FUNCTIONS, POSITION is not one of the
  157. found positions, return FALLBACK-POSITION. If it is nil, return
  158. the first position."
  159. (or position (setq position (point)))
  160. (if (null functions)
  161. fallback-position
  162. (pcase functions
  163. (`(,first . ,rest)
  164. ;; If the last function is reached, there is no point to call it,
  165. ;; as the point should be moved to the first position anyway.
  166. (if (and (null rest) fallback-position)
  167. fallback-position
  168. (let ((pos (funcall first)))
  169. (if (and pos (= pos position))
  170. (or (mwim-first-position rest position)
  171. fallback-position
  172. pos)
  173. (mwim-next-position rest position
  174. (or fallback-position pos)))))))))
  175. (defun mwim-delq-dups (list)
  176. "Like `delete-dups' but using `eq'."
  177. (let ((tail list))
  178. (while tail
  179. (setcdr tail (delq (car tail) (cdr tail)))
  180. (setq tail (cdr tail))))
  181. list)
  182. (defun mwim-next-unique-position (functions &optional position
  183. sort-predicate)
  184. "Return the next point position after POSITION from positions
  185. defined after calling FUNCTIONS.
  186. If POSITION is nil, use the current point position.
  187. Initially, all positions are calculated (all functions are
  188. called). If POSITION is the same as one of the resulting
  189. positions, return the next one, otherwise return the first
  190. position.
  191. If SORT-PREDICATE is non-nil, it should be a function taken by
  192. `sort'. It is used to sort available positions, so most likely
  193. you want to use either `<' or `>' for SORT-PREDICATE."
  194. (or position (setq position (point)))
  195. (let* ((positions (mwim-delq-dups
  196. (delq nil (mapcar #'funcall functions))))
  197. (positions (if sort-predicate
  198. (sort positions sort-predicate)
  199. positions))
  200. (next-positions (cdr (memq position positions))))
  201. (car (or next-positions positions))))
  202. (defun mwim-move-to-next-position (functions &optional sort-predicate)
  203. "Move point to position returned by the first function.
  204. If the point is already there, move to the position returned by
  205. the second function, etc.
  206. FUNCTIONS are called without arguments and should return either a
  207. number (point position) or nil (if this position should be
  208. skipped).
  209. If SORT-PREDICATE is non-nil, `mwim-next-unique-position' is
  210. called with it."
  211. (let ((pos (if sort-predicate
  212. (mwim-next-unique-position functions (point)
  213. sort-predicate)
  214. (funcall (or mwim-next-position-function
  215. (if (> (length functions) 3)
  216. #'mwim-next-unique-position
  217. #'mwim-next-position))
  218. functions))))
  219. (when pos (goto-char pos))))
  220. ;; This macro is not really needed, it is an artifact from the past. It
  221. ;; is left in case some people use it to define their commands.
  222. (defmacro mwim-goto-next-position (&rest expressions)
  223. "Wrapper for `mwim-move-to-next-position'."
  224. (declare (indent 0))
  225. `(mwim-move-to-next-position
  226. (list ,@(mapcar (lambda (exp) `(lambda () ,exp))
  227. expressions))))
  228. ;;; Position functions
  229. (defun mwim-current-comment-beginning ()
  230. "Return position of the beginning of the current comment.
  231. Return nil, if not inside a comment."
  232. (let ((syn (syntax-ppss)))
  233. (and (nth 4 syn)
  234. (nth 8 syn))))
  235. (defun mwim-line-comment-beginning ()
  236. "Return position of the beginning of comment on the current line.
  237. Return nil, if there is no comment beginning on the current line."
  238. (let ((beg (save-excursion
  239. (end-of-line)
  240. (or (mwim-current-comment-beginning)
  241. ;; There may be a marginal block comment, see
  242. ;; <https://github.com/alezost/mwim.el/issues/3>.
  243. (progn
  244. (skip-chars-backward " \t")
  245. (backward-char)
  246. (mwim-current-comment-beginning))))))
  247. (and beg
  248. (<= (line-beginning-position) beg)
  249. beg)))
  250. (defun mwim-line-comment-text-beginning ()
  251. "Return position of a comment start on the current line.
  252. Comment start means beginning of the text inside the comment.
  253. Return nil, if there is no comment beginning on the current line."
  254. ;; If `comment-start-skip' is not set by a major mode (this is the
  255. ;; case for `sql-mode', for example), then `comment-search-forward'
  256. ;; errors, so check that `comment-start-skip' is not nil.
  257. (when comment-start-skip
  258. (save-excursion
  259. (goto-char (line-beginning-position))
  260. (let ((beg (comment-search-forward (line-end-position) t)))
  261. (when beg (point))))))
  262. (defalias 'mwim-comment-beginning #'mwim-line-comment-text-beginning)
  263. (defun mwim-line-beginning ()
  264. "Return position in the beginning of line.
  265. Use `mwim-beginning-of-line-function'."
  266. (mwim-point-at (mwim-beginning-of-line)))
  267. (defun mwim-code-beginning ()
  268. "Return position in the beginning of code."
  269. (mwim-point-at (mwim-beginning-of-code)))
  270. (defun mwim-line-end ()
  271. "Return position in the end of line.
  272. Use `mwim-end-of-line-function'."
  273. (mwim-point-at (mwim-end-of-line)))
  274. (defun mwim-code-end ()
  275. "Return position in the end of code."
  276. (mwim-point-at (mwim-end-of-code)))
  277. (defun mwim-block-beginning ()
  278. "Return position in the beginning of code or comment.
  279. If the point is inside a comment, return beginning position of
  280. the current comment, otherwise - of the code."
  281. (if (mwim-current-comment-beginning)
  282. (mwim-comment-beginning)
  283. (mwim-code-beginning)))
  284. (defun mwim-block-end ()
  285. "Return position in the end of code or line.
  286. If the point is inside a comment, return end position of
  287. the current comment, otherwise - of the code."
  288. (if (mwim-current-comment-beginning)
  289. (mwim-line-end)
  290. (mwim-code-end)))
  291. ;;; Moving commands
  292. (defun mwim-beginning-of-comment ()
  293. "Move point to the beginning of comment on the current line.
  294. If the comment does not exist, do nothing."
  295. (interactive "^")
  296. (let ((comment-beg (mwim-line-comment-beginning)))
  297. (when comment-beg
  298. (goto-char comment-beg))))
  299. (defun mwim-beginning-of-line ()
  300. "Move point to the beginning of line.
  301. Use `mwim-beginning-of-line-function'."
  302. (interactive "^")
  303. (funcall (or (mwim-function mwim-beginning-of-line-function)
  304. #'beginning-of-line)))
  305. (defun mwim-end-of-line ()
  306. "Move point to the end of line.
  307. Use `mwim-end-of-line-function'."
  308. (interactive "^")
  309. (funcall (or (mwim-function mwim-end-of-line-function)
  310. #'end-of-line)))
  311. (defun mwim-beginning-of-code ()
  312. "Move point to the first non-whitespace character on the current line."
  313. (interactive "^")
  314. (mwim-beginning-of-line)
  315. (skip-syntax-forward " " (line-end-position)))
  316. (defun mwim-end-of-code ()
  317. "Move point to the end of code.
  318. 'End of code' means before a possible comment and trailing
  319. whitespaces. Comments are recognized in any mode that sets
  320. `syntax-ppss' properly.
  321. If current line is fully commented (contains only comment), move
  322. to the end of line."
  323. (interactive "^")
  324. (mwim-end-of-line)
  325. (let ((comment-beg (mwim-line-comment-beginning)))
  326. (when comment-beg
  327. (let ((eoc (mwim-point-at
  328. (goto-char comment-beg)
  329. (skip-chars-backward " \t"))))
  330. (when (< (line-beginning-position) eoc)
  331. (goto-char eoc)))))
  332. (skip-chars-backward " \t"))
  333. (defmacro mwim-define-command (position &rest objects)
  334. "Define `mwim-POSITION-of-OBJECT1-or-OBJECT2-or-...' command.
  335. POSITION is either `beginning' or `end'.
  336. OBJECT1 and OBJECT2 can be `line', `code' or `comment'."
  337. (let* ((object1 (car objects))
  338. (direct-fun (intern (format "mwim-%S-of-%S" position object1)))
  339. (fun-name (intern
  340. (concat "mwim-" (symbol-name position) "-of-"
  341. (mapconcat #'symbol-name objects "-or-")))))
  342. `(defun ,fun-name (&optional arg)
  343. ,(concat (format "Move point to the %S of %S."
  344. position object1)
  345. (mapconcat (lambda (object)
  346. (format "
  347. If the point is already there, move to the %S of %S."
  348. position object))
  349. (cdr objects) "")
  350. "\n
  351. If ARG is specified, move forward (or backward) this many lines.
  352. See `forward-line' for details.")
  353. (interactive
  354. (progn
  355. (handle-shift-selection)
  356. (when current-prefix-arg
  357. (list (prefix-numeric-value current-prefix-arg)))))
  358. (if (or (null arg) (= 0 arg))
  359. (mwim-move-to-next-position
  360. ',(mapcar (lambda (object)
  361. (intern (format "mwim-%S-%S" object position)))
  362. objects))
  363. (forward-line arg)
  364. (,direct-fun)))))
  365. (mwim-define-command beginning line code)
  366. (mwim-define-command beginning code line)
  367. (mwim-define-command beginning code line comment)
  368. (mwim-define-command end line code)
  369. (mwim-define-command end code line)
  370. ;;;###autoload (autoload 'mwim-beginning-of-line-or-code "mwim" nil t)
  371. ;;;###autoload (autoload 'mwim-beginning-of-code-or-line "mwim" nil t)
  372. ;;;###autoload (autoload 'mwim-beginning-of-code-or-line-or-comment "mwim" nil t)
  373. ;;;###autoload (autoload 'mwim-end-of-line-or-code "mwim" nil t)
  374. ;;;###autoload (autoload 'mwim-end-of-code-or-line "mwim" nil t)
  375. ;;;###autoload
  376. (defun mwim-beginning (&optional arg)
  377. "Move point to the next beginning position
  378. Available positions are defined by `mwim-beginning-position-functions'.
  379. See `mwim-move-to-next-position' for details.
  380. Interactively, with prefix argument, move to the previous position."
  381. (interactive "^P")
  382. (mwim-move-to-next-position
  383. (if arg
  384. (reverse mwim-beginning-position-functions)
  385. mwim-beginning-position-functions)))
  386. ;;;###autoload
  387. (defun mwim-end (&optional arg)
  388. "Move point to the next end position.
  389. Available positions are defined by `mwim-end-position-functions'.
  390. See `mwim-move-to-next-position' for details.
  391. Interactively, with prefix argument, move to the previous position."
  392. (interactive "^P")
  393. (mwim-move-to-next-position
  394. (if arg
  395. (reverse mwim-end-position-functions)
  396. mwim-end-position-functions)))
  397. ;;;###autoload
  398. (defun mwim (&optional arg)
  399. "Switch between various positions on the current line.
  400. Available positions are defined by `mwim-position-functions'
  401. variable.
  402. Interactively, with prefix argument, move to the previous position."
  403. (interactive "^P")
  404. (mwim-move-to-next-position mwim-position-functions
  405. (if arg #'> #'<)))
  406. (provide 'mwim)
  407. ;;; mwim.el ends here