Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

430 lines
15 KiB

  1. ;;; lispy-inline.el --- inline arglist and documentation. -*- lexical-binding: t -*-
  2. ;; Copyright (C) 2014-2015 Oleh Krehel
  3. ;; This file is not part of GNU Emacs
  4. ;; This file is free software; you can redistribute it and/or modify
  5. ;; it under the terms of the GNU General Public License as published by
  6. ;; the Free Software Foundation; either version 3, or (at your option)
  7. ;; any later version.
  8. ;; This program is distributed in the hope that it will be useful,
  9. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. ;; GNU General Public License for more details.
  12. ;; For a full copy of the GNU General Public License
  13. ;; see <http://www.gnu.org/licenses/>.
  14. ;;; Commentary:
  15. ;;
  16. ;; Display current function arguments or docstring in an in-place
  17. ;; overlay.
  18. ;;; Code:
  19. (if (version< emacs-version "26.1")
  20. (progn
  21. (defsubst string-trim-left (string &optional regexp)
  22. "Trim STRING of leading string matching REGEXP.
  23. REGEXP defaults to \"[ \\t\\n\\r]+\"."
  24. (if (string-match (concat "\\`\\(?:" (or regexp "[ \t\n\r]+") "\\)") string)
  25. (replace-match "" t t string)
  26. string))
  27. (defsubst string-trim-right (string &optional regexp)
  28. "Trim STRING of trailing string matching REGEXP.
  29. REGEXP defaults to \"[ \\t\\n\\r]+\"."
  30. (if (string-match (concat "\\(?:" (or regexp "[ \t\n\r]+") "\\)\\'") string)
  31. (replace-match "" t t string)
  32. string))
  33. (defsubst string-trim (string &optional trim-left trim-right)
  34. "Trim STRING of leading and trailing strings matching TRIM-LEFT and TRIM-RIGHT.
  35. TRIM-LEFT and TRIM-RIGHT default to \"[ \\t\\n\\r]+\"."
  36. (string-trim-left (string-trim-right string trim-right) trim-left)))
  37. (require 'subr-x))
  38. (defgroup lispy-faces nil
  39. "Font-lock faces for `lispy'."
  40. :group 'lispy
  41. :prefix "lispy-face-")
  42. (defface lispy-face-hint
  43. '((((class color) (background light))
  44. :background "#fff3bc" :foreground "black")
  45. (((class color) (background dark))
  46. :background "black" :foreground "#fff3bc"))
  47. "Basic hint face."
  48. :group 'lispy-faces)
  49. (defface lispy-face-req-nosel
  50. '((t (:inherit lispy-face-hint)))
  51. "Face for required unselected args."
  52. :group 'lispy-faces)
  53. (defface lispy-face-req-sel
  54. '((t (:inherit lispy-face-req-nosel :bold t)))
  55. "Face for required selected args."
  56. :group 'lispy-faces)
  57. (defface lispy-face-opt-nosel
  58. '((t (:inherit lispy-face-hint :slant italic)))
  59. "Face for optional unselected args."
  60. :group 'lispy-faces)
  61. (defface lispy-face-key-nosel
  62. '((t (:inherit lispy-face-hint :slant italic)))
  63. "Face for keyword unselected args."
  64. :group 'lispy-faces)
  65. (defface lispy-face-opt-sel
  66. '((t (:inherit lispy-face-opt-nosel :bold t)))
  67. "Face for optional selected args."
  68. :group 'lispy-faces)
  69. (defface lispy-face-key-sel
  70. '((t (:inherit lispy-face-opt-nosel :bold t)))
  71. "Face for keyword selected args."
  72. :group 'lispy-faces)
  73. (defface lispy-face-rst-nosel
  74. '((t (:inherit lispy-face-hint)))
  75. "Face for rest unselected args."
  76. :group 'lispy-faces)
  77. (defface lispy-face-rst-sel
  78. '((t (:inherit lispy-face-rst-nosel :bold t)))
  79. "Face for rest selected args."
  80. :group 'lispy-faces)
  81. (defcustom lispy-window-height-ratio 0.65
  82. "`lispy--show' will fail with string taller than window height times this.
  83. The caller of `lispy--show' might use a substitute e.g. `describe-function'."
  84. :type 'float
  85. :group 'lispy)
  86. (defvar lispy-elisp-modes
  87. '(emacs-lisp-mode lisp-interaction-mode eltex-mode minibuffer-inactive-mode
  88. suggest-mode)
  89. "Modes for which `lispy--eval-elisp' and related functions are appropriate.")
  90. (defvar lispy-clojure-modes
  91. '(clojure-mode clojurescript-mode clojurex-mode clojurec-mode)
  92. "Modes for which clojure related functions are appropriate.")
  93. (defvar lispy-overlay nil
  94. "Hint overlay instance.")
  95. (defvar lispy-hint-pos nil
  96. "Point position where the hint should be (re-) displayed.")
  97. (declare-function lispy--eval-clojure "le-clojure")
  98. (declare-function lispy--clojure-args "le-clojure")
  99. (declare-function lispy--clojure-resolve "le-clojure")
  100. (declare-function lispy--describe-clojure-java "le-clojure")
  101. (declare-function lispy--eval-scheme "le-scheme")
  102. (declare-function lispy--eval-lisp "le-lisp")
  103. (declare-function lispy--lisp-args "le-lisp")
  104. (declare-function lispy--lisp-describe "le-lisp")
  105. (declare-function lispy--back-to-paren "lispy")
  106. (declare-function lispy--current-function "lispy")
  107. (declare-function lispy--in-comment-p "lispy")
  108. (declare-function lispy--bounds-string "lispy")
  109. ;; ——— Commands ————————————————————————————————————————————————————————————————
  110. (defun lispy--back-to-python-function ()
  111. "Move point from function call at point to the function name."
  112. (let ((pt (point))
  113. bnd)
  114. (if (lispy--in-comment-p)
  115. (error "Not possible in a comment")
  116. (condition-case nil
  117. (progn
  118. (when (setq bnd (lispy--bounds-string))
  119. (goto-char (car bnd)))
  120. (up-list -1))
  121. (error (goto-char pt)))
  122. (unless (looking-at "\\_<")
  123. (re-search-backward "\\_<" (line-beginning-position))))))
  124. (defun lispy-arglist-inline ()
  125. "Display arglist for `lispy--current-function' inline."
  126. (interactive)
  127. (save-excursion
  128. (if (eq major-mode 'python-mode)
  129. (lispy--back-to-python-function)
  130. (lispy--back-to-paren))
  131. (unless (and (prog1 (lispy--cleanup-overlay)
  132. (when (window-minibuffer-p)
  133. (window-resize (selected-window) -1)))
  134. (= lispy-hint-pos (point)))
  135. (cond ((memq major-mode lispy-elisp-modes)
  136. (let ((sym (intern-soft (lispy--current-function))))
  137. (cond ((fboundp sym)
  138. (setq lispy-hint-pos (point))
  139. (lispy--show (lispy--pretty-args sym))))))
  140. ((or (memq major-mode '(cider-repl-mode))
  141. (memq major-mode lispy-clojure-modes))
  142. (require 'le-clojure)
  143. (setq lispy-hint-pos (point))
  144. (lispy--show (lispy--clojure-args (lispy--current-function))))
  145. ((eq major-mode 'lisp-mode)
  146. (require 'le-lisp)
  147. (setq lispy-hint-pos (point))
  148. (lispy--show (lispy--lisp-args (lispy--current-function))))
  149. ((eq major-mode 'python-mode)
  150. (require 'le-python)
  151. (setq lispy-hint-pos (point))
  152. (let ((arglist (lispy--python-arglist
  153. (python-info-current-symbol)
  154. (buffer-file-name)
  155. (line-number-at-pos)
  156. (current-column))))
  157. (while (eq (char-before) ?.)
  158. (backward-sexp))
  159. (lispy--show arglist)))
  160. (t (error "%s isn't supported currently" major-mode))))))
  161. (defvar lispy--di-window-config nil
  162. "Store window configuration before `lispy-describe-inline'.")
  163. (defun lispy--hint-pos ()
  164. "Point position for the first column of the hint."
  165. (save-excursion
  166. (cond ((region-active-p)
  167. (goto-char (region-beginning)))
  168. ((eq major-mode 'python-mode)
  169. (condition-case nil
  170. (goto-char (beginning-of-thing 'sexp))
  171. (error (up-list -1))))
  172. (t
  173. (lispy--back-to-paren)))
  174. (point)))
  175. (defun lispy--cleanup-overlay ()
  176. "Delete `lispy-overlay' if it's valid and return t."
  177. (when (overlayp lispy-overlay)
  178. (delete-overlay lispy-overlay)
  179. (setq lispy-overlay nil)
  180. t))
  181. (declare-function geiser-doc-symbol-at-point "geiser-doc")
  182. (defun lispy--describe-inline ()
  183. "Toggle the overlay hint."
  184. (condition-case nil
  185. (let ((new-hint-pos (lispy--hint-pos))
  186. doc)
  187. (if (and (eq lispy-hint-pos new-hint-pos)
  188. (overlayp lispy-overlay))
  189. (lispy--cleanup-overlay)
  190. (save-excursion
  191. (when (= 0 (count-lines (window-start) (point)))
  192. (recenter 1))
  193. (setq lispy-hint-pos new-hint-pos)
  194. (if (eq major-mode 'scheme-mode)
  195. (geiser-doc-symbol-at-point)
  196. (when (setq doc (lispy--docstring (lispy--current-function)))
  197. (goto-char lispy-hint-pos)
  198. (lispy--show (propertize doc 'face 'lispy-face-hint)))))))
  199. (error
  200. (lispy--cleanup-overlay))))
  201. (defun lispy--docstring (sym)
  202. "Get the docstring for SYM."
  203. (cond
  204. ((memq major-mode lispy-elisp-modes)
  205. (setq sym (intern-soft sym))
  206. (cond ((fboundp sym)
  207. (or (documentation sym)
  208. "undocumented"))
  209. ((boundp sym)
  210. (or (documentation-property
  211. sym 'variable-documentation)
  212. "undocumented"))
  213. (t "unbound")))
  214. ((or (memq major-mode lispy-clojure-modes)
  215. (memq major-mode '(cider-repl-mode)))
  216. (require 'le-clojure)
  217. (let ((rsymbol (lispy--clojure-resolve sym)))
  218. (string-trim-left
  219. (replace-regexp-in-string
  220. "^\\(?:-+\n\\|\n*.*$.*@.*\n*\\)" ""
  221. (cond ((stringp rsymbol)
  222. (read
  223. (lispy--eval-clojure
  224. (format "(with-out-str (clojure.repl/doc %s))" rsymbol))))
  225. ((eq rsymbol 'special)
  226. (read
  227. (lispy--eval-clojure
  228. (format "(with-out-str (clojure.repl/doc %s))" sym))))
  229. ((eq rsymbol 'keyword)
  230. "No docs for keywords")
  231. ((and (listp rsymbol)
  232. (eq (car rsymbol) 'variable))
  233. (cadr rsymbol))
  234. (t
  235. (or (lispy--describe-clojure-java sym)
  236. (format "Could't resolve '%s" sym))))))))
  237. ((eq major-mode 'lisp-mode)
  238. (require 'le-lisp)
  239. (lispy--lisp-describe sym))
  240. ((eq major-mode 'python-mode)
  241. (require 'le-python)
  242. (if sym
  243. (lispy--python-docstring sym)
  244. (require 'semantic)
  245. (semantic-mode 1)
  246. (let ((sym (semantic-ctxt-current-symbol)))
  247. (if sym
  248. (progn
  249. (setq sym (mapconcat #'identity sym "."))
  250. (or
  251. (lispy--python-docstring sym)
  252. (progn
  253. (message "no doc: %s" sym)
  254. nil)))
  255. (error "The point is not on a symbol")))))
  256. (t
  257. (format "%s isn't supported currently" major-mode))))
  258. (declare-function semantic-ctxt-current-symbol "ctxt")
  259. (defun lispy-describe-inline ()
  260. "Display documentation for `lispy--current-function' inline."
  261. (interactive)
  262. (if (cl-some
  263. (lambda (window)
  264. (equal (buffer-name (window-buffer window)) "*lispy-help*"))
  265. (window-list))
  266. (when (window-configuration-p lispy--di-window-config)
  267. (set-window-configuration lispy--di-window-config))
  268. (lispy--describe-inline)))
  269. (declare-function lispy--python-docstring "le-python")
  270. (declare-function lispy--python-arglist "le-python")
  271. (declare-function python-info-current-symbol "python")
  272. ;; ——— Utilities ———————————————————————————————————————————————————————————————
  273. (defun lispy--arglist (symbol)
  274. "Get arglist for SYMBOL."
  275. (let (doc)
  276. (if (setq doc (help-split-fundoc (documentation symbol t) symbol))
  277. (car doc)
  278. (prin1-to-string
  279. (cons symbol (help-function-arglist symbol t))))))
  280. (defun lispy--join-pad (strs width)
  281. "Join STRS padding each line with WIDTH spaces."
  282. (let* ((maxw (apply #'max (mapcar #'length strs)))
  283. (padding (make-string width ?\ ))
  284. (fstring (format "%%- %ds" maxw)))
  285. (mapconcat
  286. (lambda (x)
  287. (concat
  288. padding
  289. (let ((str (format fstring x)))
  290. (font-lock-append-text-property
  291. 0 (length str) 'face 'lispy-face-hint str)
  292. str)))
  293. strs
  294. "\n")))
  295. (defun lispy--show-fits-p (str)
  296. "Return nil if window isn't large enough to display STR whole."
  297. (let ((strs (split-string str "\n")))
  298. (when (or (< (length strs) (* lispy-window-height-ratio (window-height)))
  299. (window-minibuffer-p))
  300. strs)))
  301. (defun lispy--show (str)
  302. "Show STR hint when `lispy--show-fits-p' is t."
  303. (let ((last-point (point))
  304. (strs (lispy--show-fits-p str)))
  305. (if strs
  306. (progn
  307. (setq str (lispy--join-pad
  308. strs
  309. (+ (if (window-minibuffer-p)
  310. (- (minibuffer-prompt-end) (point-min))
  311. 0)
  312. (string-width (buffer-substring
  313. (line-beginning-position)
  314. (point))))))
  315. (save-excursion
  316. (goto-char lispy-hint-pos)
  317. (if (= -1 (forward-line -1))
  318. (setq str (concat str "\n"))
  319. (end-of-line)
  320. (setq str (concat "\n" str)))
  321. (setq str (concat str
  322. (buffer-substring (point) (1+ (point)))))
  323. (if lispy-overlay
  324. (progn
  325. (move-overlay lispy-overlay (point) (+ (point) 1))
  326. (overlay-put lispy-overlay 'invisible nil))
  327. (setq lispy-overlay (make-overlay (point) (+ (point) 1)))
  328. (overlay-put lispy-overlay 'priority 9999))
  329. (overlay-put lispy-overlay 'display str)
  330. (overlay-put lispy-overlay 'after-string "")
  331. (put 'lispy-overlay 'last-point last-point)))
  332. (setq lispy--di-window-config (current-window-configuration))
  333. (save-selected-window
  334. (pop-to-buffer (get-buffer-create "*lispy-help*"))
  335. (let ((inhibit-read-only t))
  336. (delete-region (point-min) (point-max))
  337. (insert str)
  338. (goto-char (point-min))
  339. (help-mode))))))
  340. (defun lispy--pretty-args (symbol)
  341. "Return a vector of fontified strings for function SYMBOL."
  342. (let* ((args (cdr (read (lispy--arglist symbol))))
  343. (p-opt (cl-position '&optional args :test 'equal))
  344. (p-rst (or (cl-position '&rest args :test 'equal)
  345. (cl-position-if (lambda (x)
  346. (and (symbolp x)
  347. (string-match
  348. "\\.\\.\\.\\'"
  349. (symbol-name x))))
  350. args)))
  351. (a-req (cl-subseq args 0 (or p-opt p-rst (length args))))
  352. (a-opt (and p-opt
  353. (cl-subseq args (1+ p-opt) (or p-rst (length args)))))
  354. (a-rst (and p-rst (last args))))
  355. (format
  356. "(%s)"
  357. (mapconcat
  358. #'identity
  359. (append
  360. (list (propertize (symbol-name symbol) 'face 'lispy-face-hint))
  361. (mapcar
  362. (lambda (x)
  363. (propertize (downcase (prin1-to-string x)) 'face 'lispy-face-req-nosel))
  364. a-req)
  365. (mapcar
  366. (lambda (x)
  367. (propertize (downcase (prin1-to-string x)) 'face 'lispy-face-opt-nosel))
  368. a-opt)
  369. (mapcar
  370. (lambda (x)
  371. (setq x (downcase (symbol-name x)))
  372. (unless (string-match "\\.\\.\\.$" x)
  373. (setq x (concat x "...")))
  374. (propertize x 'face 'lispy-face-rst-nosel))
  375. a-rst))
  376. " "))))
  377. (provide 'lispy-inline)
  378. ;;; Local Variables:
  379. ;;; outline-regexp: ";; ———"
  380. ;;; End:
  381. ;;; lispy-inline.el ends here