123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417 |
- ;;; ltex-eglot.el --- LTeX support for Eglot. -*- lexical-binding: t -*-
- ;;; Commentary:
- ;;; Code:
- (require 'eglot)
- (defconst ltex-eglot-supported-languages
- '("ar" "ast-ES" "be-BY" "br-FR" "ca-ES" "ca-ES-valencia" "da-DK" "de" "de-AT"
- "de-CH" "de-DE" "de-DE-x-simple-language" "el-GR" "en" "en-AU" "en-CA" "en-GB"
- "en-NZ" "en-US" "en-ZA" "eo" "es" "es-AR" "fa" "fr" "ga-IE" "gl-ES" "it"
- "ja-JP" "km-KH" "nl" "nl-BE" "pl-PL" "pt" "pt-AO" "pt-BR" "pt-MZ" "pt-PT"
- "ro-RO" "ru-RU" "sk-SK" "sl-SI" "sv" "ta-IN" "tl-PH" "uk-UA" "zh-CN")
- "List of languages supportd by LTeX.")
- (defcustom ltex-eglot-server-binary "ltex-ls"
- "The binary to use for the LTeX LSP server."
- :group 'ltex-eglot
- :type 'string)
- (defconst ltex-eglot-modes
- ;; Source:
- ;; https://github.com/emacs-languagetool/eglot-ltex/blob/master/eglot-ltex.el
- '((org-mode :language-id "org")
- (git-commit-elisp-text-mode :language-id "gitcommit")
- (bibtex-mode :language-id "bibtex")
- (context-mode :language-id "context")
- (latex-mode :language-id "latex")
- (LaTeX-mode :language-id "latex")
- (markdown-mode :language-id "markdown")
- (rst-mode :language-id "restructuredtext")
- (text-mode :language-id "plaintext"))
- "List of major mode that work with LanguageTool.")
- (defcustom ltex-eglot-mother-tounge "en-US"
- "The user's native language."
- :group 'ltex-eglot
- :type '(string :tag "Language Code"))
- (defcustom ltex-eglot-language ltex-eglot-mother-tounge
- "The main language to use when checking documents."
- :group 'ltex-eglot
- :type '(choice :tag "Language"
- (const :tag "Detect Automatically" "auto")
- (string :tag "Language Code"))
- :set-after '(ltex-eglot-mother-tounge)
- :safe 'stringp)
- (defcustom ltex-eglot-enable-spell-check nil
- "Weather or not to enable spell checking with LTeX."
- :group 'ltex-eglot
- :type '(choice :tag "Status"
- (const :tag "Enabled" t)
- (const :tag "Disabled" nil)))
- (defcustom ltex-eglot-spell-check-rules
- '(:en-US ["EN_CONTRACTION_SPELLING" "MORFOLOGIK_RULE_EN_US"])
- "Rules to disable if `ltex-eglot-enable-spell-check' is nil."
- :group 'ltex-eglot
- :type '(plist :tag "Entries by language"
- :key-type (string :tag "Language Code")
- :value-type (repeat :tag "Rules" string)))
- (defun ltex-eglot--entry-file-p (entry)
- "Check if ENTRY would be concidered a file by LTex LSP."
- (when (stringp entry)
- (string-prefix-p ":" entry)))
- (defun ltex-eglot--non-file-settings-plist-p (plist)
- "Return non-nil if none of the values of PLIST refer to files.
- This is meant to check file-local saftey for the likes of
- `ltex-eglot-disabled-rules'."
- (cl-loop for (_ entries) on plist by 'cddr
- when (cl-some 'ltex-eglot--entry-file-p entries)
- do (cl-return)
- finally return t))
- (defcustom ltex-eglot-disabled-rules ()
- "List of diagnostic rules to disable."
- :group 'ltex-eglot
- :type '(plist :tag "Entries by language"
- :key-type (string :tag "Language Code")
- :value-type (repeat :tag "Rules" string))
- :safe 'ltex-eglot--non-file-settings-plist-p)
- (defcustom ltex-eglot-enabled-rules ()
- "List of diagnostic rules to enable."
- :group 'ltex-eglot
- :type '(plist :tag "Entries by language"
- :key-type (string :tag "Language Code")
- :value-type (repeat :tag "Rules" string))
- :safe 'ltex-eglot--non-file-settings-plist-p)
- (defcustom ltex-eglot-dictionary ()
- "List of words in the LTeX dictionary."
- :group 'ltex-eglot
- :type '(plist :tag "Entries by language"
- :key-type (string :tag "Language Code")
- :value-type (repeat :tag "Words" string))
- :safe 'ltex-eglot--non-file-settings-plist-p)
- (defun ltex-eglot--valid-latex-environments-p (plist)
- "Check if PLIST is an OK value for the `ltex-eglot-latex-environemnts'."
- (cl-loop for (name handling) on plist by 'cddr
- unless (and (stringp name)
- (member handling '("ignore" "default")))
- do (cl-return)
- finally return t))
- (defcustom ltex-eglot-latex-environments ()
- "Plist controlling the handling of LaTeX environments."
- :group 'ltex-eglot
- :type '(plist
- :tag "Environments"
- :key-type (string :tag "Name")
- :value-type (choice :tag "Handling"
- (const :tag "Ignore" "ignore")
- (const :tag "Check" "default")))
- :safe 'ltex-eglot--valid-latex-plist-p)
- (defun ltex-eglot--valid-latex-commands-p (plist)
- "Check if PLIST is an OK value for the `ltex-eglot-latex-commands'."
- (cl-loop for (name handling) on plist by 'cddr
- unless (and (stringp name)
- (member handling '("ignore" "default" "dummy"
- "pluralDummy" "vowelDummy")))
- do (cl-return)
- finally return t))
- (defcustom ltex-eglot-latex-commands ()
- "Plist controlling the handling of LaTeX commands."
- :group 'ltex-eglot
- :type '(plist
- :tag "Commands"
- :key-type (string :tag "Name")
- :value-type (choice :tag "Handling"
- (const :tag "Default" "default")
- (const :tag "Ignore" "ignore")
- (const :tag "Replace with dummy word" "dummy")
- (const :tag "Replace with dummy plural word"
- "pluralDummy")
- (const :tag "Replace with dummy vowel word"
- "vowelDummy")))
- :safe 'ltex-eglot--valid-latex-plist-p)
- (defun ltex-eglot--valid-bibtex-plist-p (plist)
- "Return non-nil if PLIST is an OK value for BibTeX options."
- (cl-loop for (name handling) on plist by 'cddr
- unless (and (stringp name)
- (booleanp handling))
- do (cl-return)
- finally return t))
- (defcustom ltex-eglot-bibtex-fields ()
- "Plist controlling the handling of BibTeX fields."
- :group 'ltex-eglot
- :type '(plist
- :tag "Fields"
- :key-type (string :tag "Name")
- :value-type (choice :tag "Handling"
- (const :tag "Ignore" nil)
- (const :tag "Check" t)))
- :safe 'ltex-eglot--valid-bibtex-plist-p)
- (defcustom ltex-eglot-enable-picky-rules nil
- "Weather or not to enable picky rules."
- :group 'ltex-eglot
- :type '(choice :tag "Status"
- (const :tag "Enabled" t)
- (const :tag "Disabled" nil))
- :safe 'booleanp)
- (defcustom ltex-eglot-variable-save-method 'dir
- "How to save variables added by quick fixes.
- This is one of the following:
- - \\='dir\tSave in .dir-locals.el
- - \\='file\tSave as a file local variable
- - nil\tJust set the buffer local value, don't save the variable"
- :group 'ltex-eglot
- :type '(choice :tag "Save method"
- (const :tag "Directory local (saved)" dir)
- (const :tag "File local (saved)" file)
- (const :tag "Buffer local (not saved)" nil))
- :safe 'symbolp)
- (defvar ltex-eglot-hidden-false-positives nil
- "List of hidden false positives.
- This is intented to be set from .dir-locals.el.")
- (put 'ltex-eglot-hidden-false-positives 'safe-local-variable
- 'ltex-eglot--non-file-settings-plist-p)
- (defun ltex-eglot--merge-options-plists (value-type &rest lists)
- "Merge each of the options plist LISTS.
- The values of each of the props can be any sequence, and will be converted to
- VALUE-TYPE. Any keys will be converted to keyword symbols if they are strings."
- (let ((output))
- (dolist (list lists output)
- (cl-loop for (prop value) on list by 'cddr
- for norm-prop = (if (stringp prop)
- (intern (concat ":" prop))
- prop)
- do
- (setf (plist-get output norm-prop)
- (cl-coerce (seq-uniq
- (seq-concatenate 'list
- (plist-get output norm-prop)
- value))
- value-type))))))
- (defun ltex-eglot--process-and-add-global (global &rest lists)
- "Merge each of LISTS with `ltex-eglot--merge-options-plists'.
- If the result of the merger results in a list with the key t, merge GLOBAL in as
- well."
- (let ((merged (apply 'ltex-eglot--merge-options-plists 'vector lists)))
- (cl-loop with found-t = nil
- for (prop value) on merged by 'cddr
- when (eq prop t) do
- (setq found-t t)
- else collect prop into output
- and collect value into output
- finally return
- (if found-t
- (ltex-eglot--merge-options-plists 'vector output global)
- output))))
- (defun ltex-eglot--make-plist-props-symbols (plist)
- "Make each of PLIST's props a symbol by calling `intern' on it."
- (cl-loop for (prop value) on plist by 'cddr
- collect (if (stringp prop)
- (intern (concat ":" prop))
- prop)
- collect value))
- (defun ltex-eglot--process-bibtex-fields-plist (plist)
- "Process a PLIST that might be `ltex-eglot-bibtex-fields'."
- (cl-loop for (prop value) on plist by 'cddr
- collect (if (stringp prop)
- (intern (concat ":" prop))
- prop)
- collect (or value :json-false)))
- ;; The ltex server doesn't work with eglot when running in standard io mode
- (defclass ltex-eglot-server (eglot-lsp-server)
- ((setup-done-p :initform nil
- :accessor ltex-eglot-server--setup-done-p)
- (hidden-positives :initform nil
- :accessor ltex-eglot-server--hidden-positives)
- (dictionary :initform nil
- :accessor ltex-eglot-server--dictionary)
- (disabled-rules :initform nil
- :accessor ltex-eglot-server--disabled-rules)
- (language :initform nil
- :accessor ltex-eglot-server--language))
- "LTeX server class.")
- (cl-defmethod ltex-eglot--disabled-rules-plist ((server ltex-eglot-server))
- "Create a plist of disabled rules by language.
- SERVER is the server from which to get the rules."
- (ltex-eglot--process-and-add-global
- (default-value 'ltex-eglot-disabled-rules)
- (ltex-eglot-server--disabled-rules server)
- (and (not ltex-eglot-enable-spell-check)
- ltex-eglot-spell-check-rules)))
- (cl-defmethod ltex-eglot--setup-server ((server ltex-eglot-server))
- "Setup up SERVER for the first time."
- ;; make sure that dir local stuff is picked up
- (save-current-buffer
- (when-let ((buf (cl-first (eglot--managed-buffers server))))
- (set-buffer buf))
- (setf
- ;; merger of global values is mediated elsewhere
- (ltex-eglot-server--hidden-positives server)
- (if (local-variable-p 'ltex-eglot-hidden-false-positives)
- ltex-eglot-hidden-false-positives
- '(t))
- (ltex-eglot-server--disabled-rules server)
- (if (local-variable-p 'ltex-eglot-disabled-rules)
- ltex-eglot-disabled-rules
- '(t))
- (ltex-eglot-server--dictionary server)
- (if (local-variable-p 'ltex-eglot-dictionary)
- ltex-eglot-dictionary
- '(t))
- (ltex-eglot-server--language server) ltex-eglot-language
- (ltex-eglot-server--setup-done-p server) t)))
- (cl-defmethod ltex-eglot--build-workspace-settings-plist ((server ltex-eglot-server))
- "Build the workspace settings plist for SERVER."
- (unless (ltex-eglot-server--setup-done-p server)
- (ltex-eglot--setup-server server))
- (list
- :language (ltex-eglot-server--language server)
- :dictionary (ltex-eglot--process-and-add-global
- (default-value 'ltex-eglot-dictionary)
- (ltex-eglot-server--dictionary server))
- :disabledRules (ltex-eglot--disabled-rules-plist server)
- :enabledRules (ltex-eglot--merge-options-plists
- 'vector
- ltex-eglot-enabled-rules)
- :hiddenFalsePositives (ltex-eglot--process-and-add-global
- (default-value 'ltex-eglot-hidden-false-positives)
- (ltex-eglot-server--hidden-positives server))
- :latex (list :commands (ltex-eglot--make-plist-props-symbols
- ltex-eglot-latex-commands)
- :environments (ltex-eglot--make-plist-props-symbols
- ltex-eglot-latex-environments))
- :bibtex (list :fields (ltex-eglot--process-bibtex-fields-plist
- ltex-eglot-bibtex-fields))
- :additionalRules (list :motherTongue ltex-eglot-mother-tounge
- :enablePickyRules
- (or ltex-eglot-enabled-rules :json-false))))
- (defun ltex-eglot--cleanup-plist-for-dir-locals (plist)
- "Cleanup PLIST for use in a .dir-locals.el file."
- (cl-loop with has-global = nil
- for (prop value) on plist by 'cddr
- when (eq prop t) do
- (setq has-global t)
- else collect prop into output
- and collect value into output
- finally
- (when has-global
- (cl-callf nconc output (list t)))
- finally return output))
- (cl-defmethod ltex-eglot--set-variable ((server ltex-eglot-server)
- variable value)
- "Set VARIABLE to VALUE in each buffer for SERVER.
- Also, maybe save VARIABLE in .dir-locals.el or as a file local variable."
- (cl-case ltex-eglot-variable-save-method
- (dir (add-dir-local-variable nil variable value))
- (file (add-file-local-variable variable value)))
- (dolist (buf (eglot--managed-buffers server))
- (setf (buffer-local-value variable buf) value)))
- (defun ltex-eglot--handle-client-action (server command slot)
- "Handle the client side action COMMAND for SERVER.
- SLOT is a slot in SERVER."
- (let* ((arg (cl-case slot
- (disabled-rules :ruleIds)
- (hidden-positives :falsePositives)
- (dictionary :words)))
- (local-var (cl-case slot
- (disabled-rules 'ltex-eglot-disabled-rules)
- (hidden-positives 'ltex-eglot-hidden-false-positives)
- (dictionary 'ltex-eglot-dictionary)))
- (args (elt (plist-get command :arguments) 0))
- (newval (ltex-eglot--merge-options-plists
- 'list
- (slot-value server slot) (plist-get args arg))))
- (setf (slot-value server slot) newval)
- (ltex-eglot--set-variable server local-var newval)
- (eglot-signal-didChangeConfiguration server)))
- (cl-defmethod eglot-execute ((server ltex-eglot-server) action)
- "Handler for LTeX actions.
- ACTION is the action which to run on SERVER."
- (let ((kind (plist-get action :kind)))
- (pcase kind
- ("quickfix.ltex.disableRules"
- (ltex-eglot--handle-client-action server (plist-get action :command)
- 'disabled-rules))
- ("quickfix.ltex.hideFalsePositives"
- (ltex-eglot--handle-client-action server (plist-get action :command)
- 'hidden-positives))
- ("quickfix.ltex.addToDictionary"
- (ltex-eglot--handle-client-action server (plist-get action :command)
- 'dictionary))
- (_ (cl-call-next-method)))))
- (defun ltex-eglot--hack-server-config (oldfun server &optional path)
- "Hack the config for SERVER into the return of ODLFUN.
- PATH is the same as for OLDFUN, which is probably
- `eglot--workspace-configuration-plist'."
- (let ((conf (funcall oldfun server path)))
- (when (ltex-eglot-server-p server)
- (let ((ltex-conf (plist-get conf :ltex)))
- (cl-loop for (prop val) on
- (ltex-eglot--build-workspace-settings-plist server)
- by 'cddr
- unless (plist-member ltex-conf prop)
- do (setf (plist-get ltex-conf prop) val))
- (setf (plist-get conf :ltex) ltex-conf)))
- conf))
- (defun ltex-eglot-set-language (language server &optional no-save)
- "Set the SERVER's language to LANGUAGE.
- When called interactively, prompt for LANGUAGE. With NO-SAVE, don't save the
- language setting in any file."
- (interactive (list (completing-read "Language"
- ltex-eglot-supported-languages)
- (eglot-current-server)
- current-prefix-arg))
- (unless (ltex-eglot-server-p server)
- (user-error "Current server is not an LTeX server!"))
- (when-let ((server (eglot-current-server)))
- (setf (ltex-eglot-server--language server) language)
- (let ((ltex-eglot-variable-save-method
- (and (not no-save)
- ltex-eglot-variable-save-method)))
- (ltex-eglot--set-variable server 'ltex-eglot-language language))
- (eglot-signal-didChangeConfiguration server)))
- ;;;###autoload
- (add-to-list 'eglot-server-programs
- (cons ltex-eglot-modes
- (list
- 'ltex-eglot-server
- ltex-eglot-server-binary "--server-type" "TcpSocket"
- "--no-endless" "--port" :autoport)))
- ;;;###autoload
- (advice-add 'eglot--workspace-configuration-plist :around
- 'ltex-eglot--hack-server-config)
- (provide 'ltex-eglot)
- ;;; ltex-eglot.el ends here
|