ltex-eglot.el 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. ;;; ltex-eglot.el --- LTeX support for Eglot. -*- lexical-binding: t -*-
  2. ;;; Commentary:
  3. ;;; Code:
  4. (require 'eglot)
  5. (defconst ltex-eglot-supported-languages
  6. '("ar" "ast-ES" "be-BY" "br-FR" "ca-ES" "ca-ES-valencia" "da-DK" "de" "de-AT"
  7. "de-CH" "de-DE" "de-DE-x-simple-language" "el-GR" "en" "en-AU" "en-CA" "en-GB"
  8. "en-NZ" "en-US" "en-ZA" "eo" "es" "es-AR" "fa" "fr" "ga-IE" "gl-ES" "it"
  9. "ja-JP" "km-KH" "nl" "nl-BE" "pl-PL" "pt" "pt-AO" "pt-BR" "pt-MZ" "pt-PT"
  10. "ro-RO" "ru-RU" "sk-SK" "sl-SI" "sv" "ta-IN" "tl-PH" "uk-UA" "zh-CN")
  11. "List of languages supportd by LTeX.")
  12. (defcustom ltex-eglot-server-binary "ltex-ls"
  13. "The binary to use for the LTeX LSP server."
  14. :group 'ltex-eglot
  15. :type 'string)
  16. (defconst ltex-eglot-modes
  17. ;; Source:
  18. ;; https://github.com/emacs-languagetool/eglot-ltex/blob/master/eglot-ltex.el
  19. '((org-mode :language-id "org")
  20. (git-commit-elisp-text-mode :language-id "gitcommit")
  21. (bibtex-mode :language-id "bibtex")
  22. (context-mode :language-id "context")
  23. (latex-mode :language-id "latex")
  24. (LaTeX-mode :language-id "latex")
  25. (markdown-mode :language-id "markdown")
  26. (rst-mode :language-id "restructuredtext")
  27. (text-mode :language-id "plaintext"))
  28. "List of major mode that work with LanguageTool.")
  29. (defcustom ltex-eglot-mother-tounge "en-US"
  30. "The user's native language."
  31. :group 'ltex-eglot
  32. :type '(string :tag "Language Code"))
  33. (defcustom ltex-eglot-language ltex-eglot-mother-tounge
  34. "The main language to use when checking documents."
  35. :group 'ltex-eglot
  36. :type '(choice :tag "Language"
  37. (const :tag "Detect Automatically" "auto")
  38. (string :tag "Language Code"))
  39. :set-after '(ltex-eglot-mother-tounge)
  40. :safe 'stringp)
  41. (defcustom ltex-eglot-enable-spell-check nil
  42. "Weather or not to enable spell checking with LTeX."
  43. :group 'ltex-eglot
  44. :type '(choice :tag "Status"
  45. (const :tag "Enabled" t)
  46. (const :tag "Disabled" nil)))
  47. (defcustom ltex-eglot-spell-check-rules
  48. '(:en-US ["EN_CONTRACTION_SPELLING" "MORFOLOGIK_RULE_EN_US"])
  49. "Rules to disable if `ltex-eglot-enable-spell-check' is nil."
  50. :group 'ltex-eglot
  51. :type '(plist :tag "Entries by language"
  52. :key-type (string :tag "Language Code")
  53. :value-type (repeat :tag "Rules" string)))
  54. (defun ltex-eglot--entry-file-p (entry)
  55. "Check if ENTRY would be concidered a file by LTex LSP."
  56. (when (stringp entry)
  57. (string-prefix-p ":" entry)))
  58. (defun ltex-eglot--non-file-settings-plist-p (plist)
  59. "Return non-nil if none of the values of PLIST refer to files.
  60. This is meant to check file-local saftey for the likes of
  61. `ltex-eglot-disabled-rules'."
  62. (cl-loop for (_ entries) on plist by 'cddr
  63. when (cl-some 'ltex-eglot--entry-file-p entries)
  64. do (cl-return)
  65. finally return t))
  66. (defcustom ltex-eglot-disabled-rules ()
  67. "List of diagnostic rules to disable."
  68. :group 'ltex-eglot
  69. :type '(plist :tag "Entries by language"
  70. :key-type (string :tag "Language Code")
  71. :value-type (repeat :tag "Rules" string))
  72. :safe 'ltex-eglot--non-file-settings-plist-p)
  73. (defcustom ltex-eglot-enabled-rules ()
  74. "List of diagnostic rules to enable."
  75. :group 'ltex-eglot
  76. :type '(plist :tag "Entries by language"
  77. :key-type (string :tag "Language Code")
  78. :value-type (repeat :tag "Rules" string))
  79. :safe 'ltex-eglot--non-file-settings-plist-p)
  80. (defcustom ltex-eglot-dictionary ()
  81. "List of words in the LTeX dictionary."
  82. :group 'ltex-eglot
  83. :type '(plist :tag "Entries by language"
  84. :key-type (string :tag "Language Code")
  85. :value-type (repeat :tag "Words" string))
  86. :safe 'ltex-eglot--non-file-settings-plist-p)
  87. (defun ltex-eglot--valid-latex-environments-p (plist)
  88. "Check if PLIST is an OK value for the `ltex-eglot-latex-environemnts'."
  89. (cl-loop for (name handling) on plist by 'cddr
  90. unless (and (stringp name)
  91. (member handling '("ignore" "default")))
  92. do (cl-return)
  93. finally return t))
  94. (defcustom ltex-eglot-latex-environments ()
  95. "Plist controlling the handling of LaTeX environments."
  96. :group 'ltex-eglot
  97. :type '(plist
  98. :tag "Environments"
  99. :key-type (string :tag "Name")
  100. :value-type (choice :tag "Handling"
  101. (const :tag "Ignore" "ignore")
  102. (const :tag "Check" "default")))
  103. :safe 'ltex-eglot--valid-latex-plist-p)
  104. (defun ltex-eglot--valid-latex-commands-p (plist)
  105. "Check if PLIST is an OK value for the `ltex-eglot-latex-commands'."
  106. (cl-loop for (name handling) on plist by 'cddr
  107. unless (and (stringp name)
  108. (member handling '("ignore" "default" "dummy"
  109. "pluralDummy" "vowelDummy")))
  110. do (cl-return)
  111. finally return t))
  112. (defcustom ltex-eglot-latex-commands ()
  113. "Plist controlling the handling of LaTeX commands."
  114. :group 'ltex-eglot
  115. :type '(plist
  116. :tag "Commands"
  117. :key-type (string :tag "Name")
  118. :value-type (choice :tag "Handling"
  119. (const :tag "Default" "default")
  120. (const :tag "Ignore" "ignore")
  121. (const :tag "Replace with dummy word" "dummy")
  122. (const :tag "Replace with dummy plural word"
  123. "pluralDummy")
  124. (const :tag "Replace with dummy vowel word"
  125. "vowelDummy")))
  126. :safe 'ltex-eglot--valid-latex-plist-p)
  127. (defun ltex-eglot--valid-bibtex-plist-p (plist)
  128. "Return non-nil if PLIST is an OK value for BibTeX options."
  129. (cl-loop for (name handling) on plist by 'cddr
  130. unless (and (stringp name)
  131. (booleanp handling))
  132. do (cl-return)
  133. finally return t))
  134. (defcustom ltex-eglot-bibtex-fields ()
  135. "Plist controlling the handling of BibTeX fields."
  136. :group 'ltex-eglot
  137. :type '(plist
  138. :tag "Fields"
  139. :key-type (string :tag "Name")
  140. :value-type (choice :tag "Handling"
  141. (const :tag "Ignore" nil)
  142. (const :tag "Check" t)))
  143. :safe 'ltex-eglot--valid-bibtex-plist-p)
  144. (defcustom ltex-eglot-enable-picky-rules nil
  145. "Weather or not to enable picky rules."
  146. :group 'ltex-eglot
  147. :type '(choice :tag "Status"
  148. (const :tag "Enabled" t)
  149. (const :tag "Disabled" nil))
  150. :safe 'booleanp)
  151. (defcustom ltex-eglot-variable-save-method 'dir
  152. "How to save variables added by quick fixes.
  153. This is one of the following:
  154. - \\='dir\tSave in .dir-locals.el
  155. - \\='file\tSave as a file local variable
  156. - nil\tJust set the buffer local value, don't save the variable"
  157. :group 'ltex-eglot
  158. :type '(choice :tag "Save method"
  159. (const :tag "Directory local (saved)" dir)
  160. (const :tag "File local (saved)" file)
  161. (const :tag "Buffer local (not saved)" nil))
  162. :safe 'symbolp)
  163. (defvar ltex-eglot-hidden-false-positives nil
  164. "List of hidden false positives.
  165. This is intented to be set from .dir-locals.el.")
  166. (put 'ltex-eglot-hidden-false-positives 'safe-local-variable
  167. 'ltex-eglot--non-file-settings-plist-p)
  168. (defun ltex-eglot--merge-options-plists (value-type &rest lists)
  169. "Merge each of the options plist LISTS.
  170. The values of each of the props can be any sequence, and will be converted to
  171. VALUE-TYPE. Any keys will be converted to keyword symbols if they are strings."
  172. (let ((output))
  173. (dolist (list lists output)
  174. (cl-loop for (prop value) on list by 'cddr
  175. for norm-prop = (if (stringp prop)
  176. (intern (concat ":" prop))
  177. prop)
  178. do
  179. (setf (plist-get output norm-prop)
  180. (cl-coerce (seq-uniq
  181. (seq-concatenate 'list
  182. (plist-get output norm-prop)
  183. value))
  184. value-type))))))
  185. (defun ltex-eglot--process-and-add-global (global &rest lists)
  186. "Merge each of LISTS with `ltex-eglot--merge-options-plists'.
  187. If the result of the merger results in a list with the key t, merge GLOBAL in as
  188. well."
  189. (let ((merged (apply 'ltex-eglot--merge-options-plists 'vector lists)))
  190. (cl-loop with found-t = nil
  191. for (prop value) on merged by 'cddr
  192. when (eq prop t) do
  193. (setq found-t t)
  194. else collect prop into output
  195. and collect value into output
  196. finally return
  197. (if found-t
  198. (ltex-eglot--merge-options-plists 'vector output global)
  199. output))))
  200. (defun ltex-eglot--make-plist-props-symbols (plist)
  201. "Make each of PLIST's props a symbol by calling `intern' on it."
  202. (cl-loop for (prop value) on plist by 'cddr
  203. collect (if (stringp prop)
  204. (intern (concat ":" prop))
  205. prop)
  206. collect value))
  207. (defun ltex-eglot--process-bibtex-fields-plist (plist)
  208. "Process a PLIST that might be `ltex-eglot-bibtex-fields'."
  209. (cl-loop for (prop value) on plist by 'cddr
  210. collect (if (stringp prop)
  211. (intern (concat ":" prop))
  212. prop)
  213. collect (or value :json-false)))
  214. ;; The ltex server doesn't work with eglot when running in standard io mode
  215. (defclass ltex-eglot-server (eglot-lsp-server)
  216. ((setup-done-p :initform nil
  217. :accessor ltex-eglot-server--setup-done-p)
  218. (hidden-positives :initform nil
  219. :accessor ltex-eglot-server--hidden-positives)
  220. (dictionary :initform nil
  221. :accessor ltex-eglot-server--dictionary)
  222. (disabled-rules :initform nil
  223. :accessor ltex-eglot-server--disabled-rules)
  224. (language :initform nil
  225. :accessor ltex-eglot-server--language))
  226. "LTeX server class.")
  227. (cl-defmethod ltex-eglot--disabled-rules-plist ((server ltex-eglot-server))
  228. "Create a plist of disabled rules by language.
  229. SERVER is the server from which to get the rules."
  230. (ltex-eglot--process-and-add-global
  231. (default-value 'ltex-eglot-disabled-rules)
  232. (ltex-eglot-server--disabled-rules server)
  233. (and (not ltex-eglot-enable-spell-check)
  234. ltex-eglot-spell-check-rules)))
  235. (cl-defmethod ltex-eglot--setup-server ((server ltex-eglot-server))
  236. "Setup up SERVER for the first time."
  237. ;; make sure that dir local stuff is picked up
  238. (save-current-buffer
  239. (when-let ((buf (cl-first (eglot--managed-buffers server))))
  240. (set-buffer buf))
  241. (setf
  242. ;; merger of global values is mediated elsewhere
  243. (ltex-eglot-server--hidden-positives server)
  244. (if (local-variable-p 'ltex-eglot-hidden-false-positives)
  245. ltex-eglot-hidden-false-positives
  246. '(t))
  247. (ltex-eglot-server--disabled-rules server)
  248. (if (local-variable-p 'ltex-eglot-disabled-rules)
  249. ltex-eglot-disabled-rules
  250. '(t))
  251. (ltex-eglot-server--dictionary server)
  252. (if (local-variable-p 'ltex-eglot-dictionary)
  253. ltex-eglot-dictionary
  254. '(t))
  255. (ltex-eglot-server--language server) ltex-eglot-language
  256. (ltex-eglot-server--setup-done-p server) t)))
  257. (cl-defmethod ltex-eglot--build-workspace-settings-plist ((server ltex-eglot-server))
  258. "Build the workspace settings plist for SERVER."
  259. (unless (ltex-eglot-server--setup-done-p server)
  260. (ltex-eglot--setup-server server))
  261. (list
  262. :language (ltex-eglot-server--language server)
  263. :dictionary (ltex-eglot--process-and-add-global
  264. (default-value 'ltex-eglot-dictionary)
  265. (ltex-eglot-server--dictionary server))
  266. :disabledRules (ltex-eglot--disabled-rules-plist server)
  267. :enabledRules (ltex-eglot--merge-options-plists
  268. 'vector
  269. ltex-eglot-enabled-rules)
  270. :hiddenFalsePositives (ltex-eglot--process-and-add-global
  271. (default-value 'ltex-eglot-hidden-false-positives)
  272. (ltex-eglot-server--hidden-positives server))
  273. :latex (list :commands (ltex-eglot--make-plist-props-symbols
  274. ltex-eglot-latex-commands)
  275. :environments (ltex-eglot--make-plist-props-symbols
  276. ltex-eglot-latex-environments))
  277. :bibtex (list :fields (ltex-eglot--process-bibtex-fields-plist
  278. ltex-eglot-bibtex-fields))
  279. :additionalRules (list :motherTongue ltex-eglot-mother-tounge
  280. :enablePickyRules
  281. (or ltex-eglot-enabled-rules :json-false))))
  282. (defun ltex-eglot--cleanup-plist-for-dir-locals (plist)
  283. "Cleanup PLIST for use in a .dir-locals.el file."
  284. (cl-loop with has-global = nil
  285. for (prop value) on plist by 'cddr
  286. when (eq prop t) do
  287. (setq has-global t)
  288. else collect prop into output
  289. and collect value into output
  290. finally
  291. (when has-global
  292. (cl-callf nconc output (list t)))
  293. finally return output))
  294. (cl-defmethod ltex-eglot--set-variable ((server ltex-eglot-server)
  295. variable value)
  296. "Set VARIABLE to VALUE in each buffer for SERVER.
  297. Also, maybe save VARIABLE in .dir-locals.el or as a file local variable."
  298. (cl-case ltex-eglot-variable-save-method
  299. (dir (add-dir-local-variable nil variable value))
  300. (file (add-file-local-variable variable value)))
  301. (dolist (buf (eglot--managed-buffers server))
  302. (setf (buffer-local-value variable buf) value)))
  303. (defun ltex-eglot--handle-client-action (server command slot)
  304. "Handle the client side action COMMAND for SERVER.
  305. SLOT is a slot in SERVER."
  306. (let* ((arg (cl-case slot
  307. (disabled-rules :ruleIds)
  308. (hidden-positives :falsePositives)
  309. (dictionary :words)))
  310. (local-var (cl-case slot
  311. (disabled-rules 'ltex-eglot-disabled-rules)
  312. (hidden-positives 'ltex-eglot-hidden-false-positives)
  313. (dictionary 'ltex-eglot-dictionary)))
  314. (args (elt (plist-get command :arguments) 0))
  315. (newval (ltex-eglot--merge-options-plists
  316. 'list
  317. (slot-value server slot) (plist-get args arg))))
  318. (setf (slot-value server slot) newval)
  319. (ltex-eglot--set-variable server local-var newval)
  320. (eglot-signal-didChangeConfiguration server)))
  321. (cl-defmethod eglot-execute ((server ltex-eglot-server) action)
  322. "Handler for LTeX actions.
  323. ACTION is the action which to run on SERVER."
  324. (let ((kind (plist-get action :kind)))
  325. (pcase kind
  326. ("quickfix.ltex.disableRules"
  327. (ltex-eglot--handle-client-action server (plist-get action :command)
  328. 'disabled-rules))
  329. ("quickfix.ltex.hideFalsePositives"
  330. (ltex-eglot--handle-client-action server (plist-get action :command)
  331. 'hidden-positives))
  332. ("quickfix.ltex.addToDictionary"
  333. (ltex-eglot--handle-client-action server (plist-get action :command)
  334. 'dictionary))
  335. (_ (cl-call-next-method)))))
  336. (defun ltex-eglot--hack-server-config (oldfun server &optional path)
  337. "Hack the config for SERVER into the return of ODLFUN.
  338. PATH is the same as for OLDFUN, which is probably
  339. `eglot--workspace-configuration-plist'."
  340. (let ((conf (funcall oldfun server path)))
  341. (when (ltex-eglot-server-p server)
  342. (let ((ltex-conf (plist-get conf :ltex)))
  343. (cl-loop for (prop val) on
  344. (ltex-eglot--build-workspace-settings-plist server)
  345. by 'cddr
  346. unless (plist-member ltex-conf prop)
  347. do (setf (plist-get ltex-conf prop) val))
  348. (setf (plist-get conf :ltex) ltex-conf)))
  349. conf))
  350. (defun ltex-eglot-set-language (language server &optional no-save)
  351. "Set the SERVER's language to LANGUAGE.
  352. When called interactively, prompt for LANGUAGE. With NO-SAVE, don't save the
  353. language setting in any file."
  354. (interactive (list (completing-read "Language"
  355. ltex-eglot-supported-languages)
  356. (eglot-current-server)
  357. current-prefix-arg))
  358. (unless (ltex-eglot-server-p server)
  359. (user-error "Current server is not an LTeX server!"))
  360. (when-let ((server (eglot-current-server)))
  361. (setf (ltex-eglot-server--language server) language)
  362. (let ((ltex-eglot-variable-save-method
  363. (and (not no-save)
  364. ltex-eglot-variable-save-method)))
  365. (ltex-eglot--set-variable server 'ltex-eglot-language language))
  366. (eglot-signal-didChangeConfiguration server)))
  367. ;;;###autoload
  368. (add-to-list 'eglot-server-programs
  369. (cons ltex-eglot-modes
  370. (list
  371. 'ltex-eglot-server
  372. ltex-eglot-server-binary "--server-type" "TcpSocket"
  373. "--no-endless" "--port" :autoport)))
  374. ;;;###autoload
  375. (advice-add 'eglot--workspace-configuration-plist :around
  376. 'ltex-eglot--hack-server-config)
  377. (provide 'ltex-eglot)
  378. ;;; ltex-eglot.el ends here