inferior-cc.el 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. ;;; inferior-cc.el --- Run interpreters for cc-mode languages -*- lexical-binding: t; -*-
  2. ;;; Commentary:
  3. ;;; Code:
  4. (require 'comint)
  5. (require 'cl-lib)
  6. (require 'cc-mode)
  7. (require 'treesit)
  8. (require 'shell)
  9. (eval-when-compile (require 'rx))
  10. (defgroup inferior-cc ()
  11. "Run interpreters for `cc-mode' languages."
  12. :group 'comint)
  13. (defclass inferior-cc-interpreter ()
  14. ((name :type string
  15. :initarg :name
  16. :accessor inf-cc-name
  17. :doc "The name of this interpreter.")
  18. (command :type string
  19. :initarg :command
  20. :accessor inf-cc-command
  21. :doc "The command (program) for this interpreter.")
  22. (args :type (list-of string)
  23. :initarg :args
  24. :accessor inf-cc-args
  25. :initform nil
  26. :doc "Command-line arguments to pass to the interpreter.")
  27. (font-lock-mode :type (or null function)
  28. :initarg :font-lock-mode
  29. :accessor inf-cc-font-lock-mode
  30. :initform nil
  31. :doc "Major mode to use for font locking of the interpreter's
  32. input. A value of nil means don't do font locking.")
  33. (modes :type (list-of function)
  34. :initarg :modes
  35. :accessor inf-cc-modes
  36. :initform nil
  37. :doc "The major modes that this interpreter corresponds to.")
  38. (exp-at-point-func :type (or function null)
  39. :initarg :exp-at-point-func
  40. :accessor inf-cc-exp-at-point-func
  41. :initform nil
  42. :doc "Function to retrieve the expression at point for
  43. languages supported by this interpreter."))
  44. (:documentation "An interpreter for a `cc-mode'-like language."))
  45. (define-widget 'inferior-cc-interpreter 'lazy
  46. "Interpreter for `cc-mode'-like languages."
  47. :offset 4
  48. :tag "Interpreter"
  49. :type '(list (string :tag "Name")
  50. (repeat :tag "Command line" (string :tag "Argument"))
  51. (choice :tag "Font lock mode"
  52. (function :tag "Major mode")
  53. (const :tag "None" nil))
  54. (repeat :tag "Major modes" (function :tag "Major mode"))
  55. (choice :tag "Expression at point function"
  56. (function :tag "Function")
  57. (const :tag "None" nil))))
  58. (defun inf-cc--interpreter-list-to-obj (list)
  59. "Return LIST as a proper `inferior-cc-interpreter' object."
  60. (cl-destructuring-bind (name (command &rest args) font-lock-mode modes
  61. exp-at-point-func)
  62. list
  63. (inferior-cc-interpreter :name name :command command
  64. :args args :font-lock-mode font-lock-mode
  65. :modes modes :exp-at-point-func exp-at-point-func)))
  66. (defun inf-cc--interpreter-obj-to-list (obj)
  67. "Return OBJ, a proper `inferior-cc-interpreter', object as a list."
  68. (with-slots (name command args font-lock-mode modes exp-at-point-func) obj
  69. (list name (cons command args) font-lock-mode modes exp-at-point-func)))
  70. (defun inf-cc--remove-trailing-semicolon (str)
  71. "Remove a trailing semicolon and whitespace from STR."
  72. (if (string-match (rx (* (syntax whitespace))
  73. ";"
  74. (* (syntax whitespace)) eos)
  75. str)
  76. (substring str 0 (match-beginning 0))
  77. str))
  78. (defun inf-cc--remove-surrounding-parens (str)
  79. "Remove surrounding parenthesis from STR."
  80. (if (string-match (rx bos (* (syntax whitespace)) "("
  81. (group (* any))
  82. ")" (* (syntax whitespace)) eos)
  83. str)
  84. (match-string 1 str)
  85. str))
  86. (defun inf-cc--c-c++-ts-exp-at-point ()
  87. "Return the expression at point in `c-ts-mode' and `c++-ts-mode' buffers."
  88. (unless (or (derived-mode-p 'c-ts-mode 'c++-ts-mode))
  89. (user-error "Major mode does not support find expressions: %s" major-mode))
  90. (save-excursion
  91. (let ((start (point)))
  92. (back-to-indentation)
  93. (unless (> (point) start)
  94. (goto-char start)))
  95. (when-let ((thing (treesit-thing-at-point "_" 'nested)))
  96. (inf-cc--remove-trailing-semicolon (treesit-node-text thing)))))
  97. (defun inf-cc--java-ts-exp-at-point ()
  98. "Return the expression at point in `java-ts-mode' buffers."
  99. (unless (or (derived-mode-p 'java-ts-mode))
  100. (user-error "Major mode does not support find expressions: %s" major-mode))
  101. (save-excursion
  102. (let ((start (point)))
  103. (back-to-indentation)
  104. (unless (> (point) start)
  105. (goto-char start)))
  106. (let ((root (treesit-buffer-root-node)))
  107. (let ((node (car (or (treesit-query-range
  108. root '([(expression_statement)
  109. (field_declaration)
  110. (local_variable_declaration)
  111. (import_declaration)]
  112. @exp)
  113. (point) (1+ (point)))
  114. (treesit-query-range
  115. root '([(parenthesized_expression)
  116. (binary_expression)
  117. (update_expression)
  118. (unary_expression)]
  119. @exp)
  120. (point) (1+ (point)))))))
  121. (inf-cc--remove-surrounding-parens
  122. (inf-cc--remove-trailing-semicolon
  123. (buffer-substring-no-properties (car node) (cdr node))))))))
  124. (defcustom inferior-cc-interpreters
  125. (list (inferior-cc-interpreter :name "jshell"
  126. :command "jshell"
  127. :font-lock-mode 'java-mode
  128. :modes '(java-mode java-ts-mode)
  129. :exp-at-point-func
  130. 'inf-cc--java-ts-exp-at-point)
  131. (inferior-cc-interpreter :name "root"
  132. :command "root"
  133. :font-lock-mode 'c++-mode
  134. :modes '(c-mode c-ts-mode c++-mode c++-ts-mode)
  135. :exp-at-point-func
  136. 'inf-cc--c-c++-ts-exp-at-point))
  137. "List of inferior-cc interpreters."
  138. :type '(repeat inferior-cc-interpreter)
  139. :get (lambda (sym)
  140. (mapcar 'inf-cc--interpreter-obj-to-list (default-toplevel-value sym)))
  141. :set (lambda (sym newval)
  142. (set-default-toplevel-value
  143. sym (mapcar #'(lambda (elt)
  144. (if (inferior-cc-interpreter-p elt)
  145. elt
  146. (inf-cc--interpreter-list-to-obj elt)))
  147. newval)))
  148. :group 'inferior-cc)
  149. (defvar-local inf-cc--obj nil
  150. "The current buffer's interpreter object.")
  151. (put 'inf-cc--obj 'permanent-local t)
  152. (defvar-local inf-cc--fontification-buffer nil
  153. "The fontification buffer for the current buffer.")
  154. (defvar-local inf-cc--skip-next-lines 0
  155. "Number of lines of output to skip.")
  156. (defun inf-cc--preoutput-filter-function (output)
  157. "Preoutput filter function for inferior cc buffers.
  158. OUTPUT is the new text to be inserted."
  159. (if (<= inf-cc--skip-next-lines 0)
  160. output
  161. (let* ((lines (string-lines output))
  162. (cnt (length lines)))
  163. (if (> cnt inf-cc--skip-next-lines)
  164. (prog1
  165. (string-join (nthcdr inf-cc--skip-next-lines lines) "\n")
  166. (setq inf-cc--skip-next-lines 0))
  167. (cl-decf inf-cc--skip-next-lines cnt)
  168. (when (and (not (string-empty-p output))
  169. (/= ?\n (elt output (1- (length output)))))
  170. (cl-incf inf-cc--skip-next-lines))
  171. ""))))
  172. (defun inf-cc--get-fontification-buffer ()
  173. "Return or create the current buffer's fontification buffer."
  174. (if (buffer-live-p inf-cc--fontification-buffer)
  175. inf-cc--fontification-buffer
  176. (let ((buffer (generate-new-buffer
  177. (format " %s-fontification-buffer" (buffer-name))))
  178. (obj inf-cc--obj))
  179. (with-current-buffer buffer
  180. (setq-local inf-cc--obj obj)
  181. (unless (and (inf-cc-font-lock-mode inf-cc--obj)
  182. (derived-mode-p (inf-cc-font-lock-mode inf-cc--obj)))
  183. (let ((delayed-mode-hooks nil))
  184. (delay-mode-hooks
  185. (funcall (inf-cc-font-lock-mode inf-cc--obj)))))
  186. (when (eq c-basic-offset 'set-from-style)
  187. (setq-local c-basic-offset standard-indent))
  188. (let ((inhibit-message t))
  189. (indent-tabs-mode -1))
  190. (unless font-lock-mode
  191. (font-lock-mode 1)))
  192. (setq-local inf-cc--fontification-buffer buffer))))
  193. (defmacro inf-cc--with-font-lock-buffer (&rest body)
  194. "Execute BODY in the current buffer's fortification buffer.
  195. Note that this erases the buffer before doing anything."
  196. `(with-current-buffer (inf-cc--get-fontification-buffer)
  197. (erase-buffer)
  198. ,@body))
  199. (defun inf-cc--fontify-current-input ()
  200. "Function called from `post-command-hook' to fontify the current input."
  201. (when-let (((inf-cc-font-lock-mode inf-cc--obj))
  202. (proc (get-buffer-process (current-buffer)))
  203. (start (process-mark proc))
  204. (end (point-max))
  205. (input (buffer-substring-no-properties start end))
  206. (fontified (inf-cc--with-font-lock-buffer
  207. (insert input)
  208. (font-lock-ensure)
  209. (buffer-string)))
  210. (len (length fontified))
  211. (i 0))
  212. ;; mostly from:
  213. ;; `python-shell-font-lock-post-command-hook'
  214. (while (not (= i len))
  215. (let* ((props (text-properties-at i fontified))
  216. (change-i (or (next-property-change i fontified)
  217. len)))
  218. (when-let ((face (plist-get props 'face)))
  219. (setf (plist-get props 'face) nil
  220. (plist-get props 'font-lock-face) face))
  221. (set-text-properties (+ start i) (+ start change-i) props)
  222. (setq i change-i)))))
  223. (defun inf-cc--bounds-of-last-prompt ()
  224. "Return the bounds of the last prompt.
  225. This returns a cons."
  226. (save-excursion
  227. (let ((end (process-mark (get-buffer-process (current-buffer)))))
  228. (goto-char end)
  229. (cons (pos-bol) end))))
  230. (defun inf-cc--remove-extra-indentation (count)
  231. "Remove COUNT spaces from the start of each line."
  232. (save-excursion
  233. (goto-char (point-min))
  234. (while (not (eobp))
  235. (back-to-indentation)
  236. (let ((indent (- (point) (pos-bol))))
  237. (when (> indent count)
  238. (delete-char (- count))))
  239. (forward-line))))
  240. (defun inf-cc--indent-line-function ()
  241. "`indent-line-function' for inferior cc comint buffers."
  242. (when (inf-cc-font-lock-mode inf-cc--obj)
  243. (let* ((start (process-mark (get-buffer-process (current-buffer)))))
  244. ;; don't indent the first line
  245. (unless (= (pos-bol) (save-excursion (goto-char start) (pos-bol)))
  246. (let* ((input (buffer-substring-no-properties start (pos-eol)))
  247. (prompt-size (let ((bound (inf-cc--bounds-of-last-prompt)))
  248. (- (cdr bound) (car bound))))
  249. (col (inf-cc--with-font-lock-buffer
  250. (insert input)
  251. (inf-cc--remove-extra-indentation prompt-size)
  252. (c-indent-line nil t)
  253. (back-to-indentation)
  254. (- (point) (pos-bol)))))
  255. (save-excursion
  256. (indent-line-to (+ prompt-size col)))
  257. (skip-syntax-forward "-"))))))
  258. (defun inferior-cc-send-input ()
  259. "Like `comint-send-input', but with some extra stuff for inferior cc."
  260. (interactive)
  261. (let ((pmark (process-mark (get-buffer-process (current-buffer))))
  262. (end (if comint-eol-on-send (pos-eol) (point))))
  263. (with-restriction pmark end
  264. (let ((res (syntax-ppss (point-max))))
  265. (without-restriction
  266. (cond
  267. ;; open string
  268. ((cl-fourth res)
  269. (message "Unterminated string"))
  270. ;; unmatched blocks or comment
  271. ((or (numberp (cl-fifth res))
  272. (not (zerop (cl-first res)))
  273. ;; trailing . character
  274. (save-excursion
  275. (end-of-line)
  276. (skip-syntax-backward "-")
  277. (eql (char-before) ?.)))
  278. (newline-and-indent))
  279. (t
  280. ;; ignore the interpreter echoing back our lines
  281. (setq-local inf-cc--skip-next-lines (count-lines pmark end))
  282. (when (= pmark end)
  283. (cl-incf inf-cc--skip-next-lines))
  284. ;; also, methods add a bunch of extra newlines
  285. (when (>= inf-cc--skip-next-lines 2)
  286. (cl-incf inf-cc--skip-next-lines (- inf-cc--skip-next-lines 2)))
  287. (comint-send-input))))))))
  288. (defvar-keymap inferior-cc-shell-mode-map
  289. :doc "Keymap for `inferior-cc-shell-mode'."
  290. :parent comint-mode-map
  291. "RET" #'inferior-cc-send-input)
  292. (defun inf-cc--kill-fontification-buffer ()
  293. "Kill the current `inf-cc--fontification-buffer'."
  294. (ignore-errors
  295. (kill-buffer inf-cc--fontification-buffer)))
  296. (define-derived-mode inferior-cc-shell-mode comint-mode ""
  297. "Major mode for buffers running inferior cc interpreters.
  298. You MUST set `inf-cc--obj' before activating this major mode."
  299. :interactive nil
  300. :group 'inferior-jshell
  301. :syntax-table nil
  302. (with-slots (name font-lock-mode) inf-cc--obj
  303. (setq-local comint-highlight-input nil
  304. indent-line-function #'inf-cc--indent-line-function
  305. electric-indent-chars '(?\n ?})
  306. mode-name (concat "Inferior " (upcase-initials name)))
  307. (when-let ((font-lock-mode)
  308. (sym (intern-soft (format "%s-syntax-table" font-lock-mode)))
  309. (syntax-table (symbol-value sym)))
  310. (set-syntax-table syntax-table)))
  311. (add-hook 'comint-preoutput-filter-functions
  312. #'inf-cc--preoutput-filter-function
  313. nil t)
  314. (add-hook 'post-command-hook
  315. #'inf-cc--fontify-current-input
  316. nil t)
  317. (add-hook 'kill-buffer-hook
  318. #'inf-cc--kill-fontification-buffer
  319. nil t))
  320. (cl-defun inf-cc--find-buffer ()
  321. "Find and return a live inferior cc buffer for the current major mode."
  322. (let ((target-mode major-mode))
  323. (dolist (buffer (buffer-list))
  324. (with-current-buffer buffer
  325. (when (and (process-live-p (get-buffer-process buffer))
  326. inf-cc--obj
  327. (member target-mode (inf-cc-modes inf-cc--obj)))
  328. (cl-return-from inf-cc--find-buffer buffer))))))
  329. (defun inferior-cc-eval (code)
  330. "Evaluate CODE in a live inferior cc buffer."
  331. (interactive "sEval: " inferior-cc-shell-mode)
  332. (let ((buffer (inf-cc--find-buffer)))
  333. (unless buffer
  334. (user-error "No live inferior cc buffer found"))
  335. (with-current-buffer buffer
  336. (let* ((start (process-mark (get-buffer-process buffer)))
  337. (end (point-max))
  338. (old (buffer-substring-no-properties start end)))
  339. (delete-region start end)
  340. (goto-char (point-max))
  341. (insert code)
  342. (goto-char (point-max))
  343. ;; don't save history
  344. (let ((comint-input-filter #'ignore))
  345. (inferior-cc-send-input))
  346. (goto-char (point-max))
  347. (insert old)
  348. (goto-char (point-max))))))
  349. (defun inferior-cc-eval-region (start end)
  350. "Evaluate the current buffer from START to END in a live inferior cc buffer.
  351. START and END default to the current region."
  352. (interactive "r" inferior-cc-shell-mode)
  353. (inferior-cc-eval (buffer-substring-no-properties start end))
  354. (message "Evaluated %s lines" (count-lines start end)))
  355. (defun inferior-cc-eval-buffer ()
  356. "Send the current buffer to a live inferior cc buffer."
  357. (interactive nil inferior-cc-shell-mode)
  358. (inferior-cc-eval-region (point-min) (point-max))
  359. (message "Evaluated buffer %s" (current-buffer)))
  360. (defun inferior-cc-eval-defun ()
  361. "Send the defun under point to a live inferior cc buffer."
  362. (interactive nil inferior-cc-shell-mode)
  363. (let ((bounds (bounds-of-thing-at-point 'defun)))
  364. (unless bounds
  365. (user-error "No defun under point"))
  366. (inferior-cc-eval-region (car bounds) (cdr bounds))
  367. (message "Evaluated defun (%s lines)" (count-lines (car bounds)
  368. (cdr bounds)))))
  369. (defun inferior-cc-eval-line ()
  370. "Send the line under point to a live inferior cc buffer."
  371. (interactive nil inferior-cc-shell-mode)
  372. (inferior-cc-eval-region (pos-bol) (pos-eol))
  373. (message "Evaluated %s" (buffer-substring (pos-bol) (pos-eol))))
  374. (defun inferior-cc-eval-expression ()
  375. "Evaluate the expression under point in a live inferior cc buffer.
  376. This only works in modes that have defined an \\=:exp-at-point-func."
  377. (interactive nil inferior-cc-shell-mode)
  378. (let ((obj (inf-cc--find-interpreter-for-mode)))
  379. (unless obj
  380. (user-error "Cannot get expression for major mode: %s" major-mode))
  381. (with-slots ((func exp-at-point-func)) obj
  382. (unless func
  383. (user-error "Cannot get expression for major mode: %s" major-mode))
  384. (let ((code (funcall func)))
  385. (unless code
  386. (user-error "No expression under point"))
  387. (inferior-cc-eval code)
  388. (message "Evaluated expression (%s lines)"
  389. (1+ (cl-count ?\n code)))))))
  390. (defun inf-cc--find-interpreter-for-mode (&optional mode)
  391. "Find a suitable interpreter for MODE, defaulting to `major-mode'."
  392. (unless mode (setq mode major-mode))
  393. (cl-find-if (lambda (elt)
  394. (with-slots (modes) elt
  395. (member mode modes)))
  396. inferior-cc-interpreters))
  397. (defun inf-cc--interpreter-by-name (name)
  398. "Find the interpreter named NAME."
  399. (cl-find-if (lambda (elt)
  400. (equal (inf-cc-name elt) name))
  401. inferior-cc-interpreters))
  402. (defun inf-cc--prompt-for-interpreter ()
  403. "Prompt for an inferior cc interpreter."
  404. (inf-cc--interpreter-by-name
  405. (completing-read "Interpreter: "
  406. (mapcar 'inf-cc-name inferior-cc-interpreters) nil t)))
  407. (defun inf-cc--prompt-for-command (int)
  408. "Prompt for a command line for INT."
  409. (with-slots (command args) int
  410. (let* ((def-cmd (string-join (mapcar 'shell-quote-argument
  411. (cons command args))
  412. " "))
  413. (choice (read-shell-command "Command: " def-cmd)))
  414. (split-string-shell-command choice))))
  415. (defun run-cc-interpreter (int &optional command)
  416. "Run the `cc-mode'-like interpreter INT.
  417. Interactively, INT will be an interpreter suitable for the current
  418. `major-mode'. With a prefix argument, prompt for an interpreter.
  419. If COMMAND is non-nil, it should be a list with the first element being the
  420. program to execute and the rest of the elements being the arguments to pass to
  421. the interpreter. This overrides the default settings in INT. Interactively,
  422. prompt for COMMAND with two prefix arguments."
  423. (interactive (let ((int (if current-prefix-arg
  424. (inf-cc--prompt-for-interpreter)
  425. (or (inf-cc--find-interpreter-for-mode)
  426. (inf-cc--prompt-for-interpreter)))))
  427. (list int
  428. (when (>= (prefix-numeric-value current-prefix-arg) 16)
  429. (inf-cc--prompt-for-command int)))))
  430. (with-slots (name (def-cmd command) args) int
  431. (unless command
  432. (setq command (cons def-cmd args)))
  433. (pop-to-buffer
  434. (with-current-buffer (get-buffer-create (format "*%s*" name))
  435. (prog1 (current-buffer)
  436. (unless (process-live-p (get-buffer-process (current-buffer)))
  437. (setq-local inf-cc--obj int)
  438. (inferior-cc-shell-mode)
  439. (comint-exec (current-buffer)
  440. (format "Inferior %s" (upcase-initials name))
  441. (car command) nil (cdr command))))))))
  442. (defun run-jshell (command)
  443. "Run JShell in a comint buffer.
  444. COMMAND is the same as for `run-cc-interpreter', except that any prefix arg
  445. causes the user to be prompted."
  446. (interactive (list (when current-prefix-arg
  447. (inf-cc--prompt-for-command
  448. (inf-cc--interpreter-by-name "jshell")))))
  449. (run-cc-interpreter (inf-cc--interpreter-by-name "jshell") command))
  450. (defun run-root (command)
  451. "Run CERN root in a comint buffer.
  452. COMMAND is the same as for `run-cc-interpreter', except that any prefix arg
  453. causes the user to be prompted."
  454. (interactive (list (when current-prefix-arg
  455. (inf-cc--prompt-for-command
  456. (inf-cc--interpreter-by-name "root")))))
  457. (run-cc-interpreter (inf-cc--interpreter-by-name "root") command))
  458. (provide 'inferior-cc)
  459. ;;; inferior-cc.el ends here