diff --git a/README.md b/README.md index 8cdb2f3e8..2aa3a7ad1 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Check out [the FAQ][FAQ] for answers to common questions about the project. - Optional vim emulation powered by [evil-mode], including ports of popular vim plugins like [vim-sneak], [vim-easymotion], [vim-unimpaired] and [more][ported-vim-plugins]! -- Opt-in LSP integration for many languages, using [lsp-mode]. +- Opt-in LSP integration for many languages, using [lsp-mode] or [eglot] - Support for *many* programming languages. Includes syntax highlighting, linters/checker integration, inline code evaluation, code completion (where possible), REPLs, documentation lookups, snippets, and more! @@ -240,6 +240,7 @@ kind! [helm]: https://github.com/emacs-helm/helm [ivy]: https://github.com/abo-abo/swiper [lsp-mode]: https://github.com/emacs-lsp/lsp-mode +[eglot]: https://github.com/joaotavora/eglot [nix]: https://nixos.org [ported-vim-plugins]: modules/editor/evil/README.org#ported-vim-plugins [ripgrep]: https://github.com/BurntSushi/ripgrep diff --git a/modules/config/default/+evil-bindings.el b/modules/config/default/+evil-bindings.el index 74b90c0a0..0c61aac43 100644 --- a/modules/config/default/+evil-bindings.el +++ b/modules/config/default/+evil-bindings.el @@ -343,32 +343,38 @@ ;;; c --- code (:prefix-map ("c" . "code") - :desc "Compile" "c" #'compile - :desc "Recompile" "C" #'recompile - :desc "Jump to definition" "d" #'+lookup/definition - :desc "Jump to references" "D" #'+lookup/references - :desc "Evaluate buffer/region" "e" #'+eval/buffer-or-region - :desc "Evaluate & replace region" "E" #'+eval:replace-region - :desc "Format buffer/region" "f" #'+format/region-or-buffer - (:when (featurep! :completion ivy) - :desc "Jump to symbol in current workspace" "j" #'lsp-ivy-workspace-symbol - :desc "Jump to symbol in any workspace" "J" #'lsp-ivy-global-workspace-symbol) - (:when (featurep! :completion helm) - :desc "Jump to symbol in current workspace" "j" #'helm-lsp-workspace-symbol - :desc "Jump to symbol in any workspace" "J" #'helm-lsp-global-workspace-symbol) - :desc "Jump to documentation" "k" #'+lookup/documentation - (:when (featurep! :tools lsp) - :desc "LSP Execute code action" "a" #'lsp-execute-code-action - :desc "LSP Organize imports" "i" #'lsp-organize-imports - :desc "LSP Rename" "r" #'lsp-rename - (:after lsp-mode - :desc "LSP" "l" lsp-command-map)) - :desc "Send to repl" "s" #'+eval/send-region-to-repl - :desc "Delete trailing whitespace" "w" #'delete-trailing-whitespace - :desc "Delete trailing newlines" "W" #'doom/delete-trailing-newlines - :desc "List errors" "x" #'flymake-show-diagnostics-buffer - (:when (featurep! :checkers syntax) - :desc "List errors" "x" #'flycheck-list-errors)) + (:unless (featurep! :tools lsp +eglot) + :desc "LSP Execute code action" "a" #'lsp-execute-code-action + :desc "LSP Organize imports" "i" #'lsp-organize-imports + (:when (featurep! :completion ivy) + :desc "Jump to symbol in current workspace" "j" #'lsp-ivy-workspace-symbol + :desc "Jump to symbol in any workspace" "J" #'lsp-ivy-global-workspace-symbol) + (:when (featurep! :completion helm) + :desc "Jump to symbol in current workspace" "j" #'helm-lsp-workspace-symbol + :desc "Jump to symbol in any workspace" "J" #'helm-lsp-global-workspace-symbol) + :desc "LSP Rename" "r" #'lsp-rename + (:after lsp-mode + :desc "LSP" "l" lsp-command-map)) + (:when (featurep! :tools lsp +eglot) + :desc "LSP Execute code action" "a" #'eglot-code-actions + :desc "LSP Format buffer/region" "F" #'eglot-format + :desc "LSP Rename" "r" #'eglot-rename + :desc "LSP Find declaration" "j" #'eglot-find-declaration + :desc "LSP Find implementation" "J" #'eglot-find-implementation) + :desc "Compile" "c" #'compile + :desc "Recompile" "C" #'recompile + :desc "Jump to definition" "d" #'+lookup/definition + :desc "Jump to references" "D" #'+lookup/references + :desc "Evaluate buffer/region" "e" #'+eval/buffer-or-region + :desc "Evaluate & replace region" "E" #'+eval:replace-region + :desc "Format buffer/region" "f" #'+format/region-or-buffer + :desc "Jump to documentation" "k" #'+lookup/documentation + :desc "Send to repl" "s" #'+eval/send-region-to-repl + :desc "Delete trailing whitespace" "w" #'delete-trailing-whitespace + :desc "Delete trailing newlines" "W" #'doom/delete-trailing-newlines + :desc "List errors" "x" #'flymake-show-diagnostics-buffer + (:when (featurep! :checkers syntax) + :desc "List errors" "x" #'flycheck-list-errors)) ;;; f --- file (:prefix-map ("f" . "file") diff --git a/modules/lang/cc/config.el b/modules/lang/cc/config.el index bab3c4cfa..33daf41a3 100644 --- a/modules/lang/cc/config.el +++ b/modules/lang/cc/config.el @@ -236,9 +236,45 @@ If rtags or rdm aren't available, fail silently instead of throwing a breaking e (setq-local company-lsp-cache-candidates nil) (lsp!)))) +(when (and (featurep! +lsp) (featurep! :tools lsp +eglot)) + ;; TODO : test this value + ;; IS-MAC custom configuration + (when IS-MAC + (add-to-list 'eglot-workspace-configuration + ((:ccls . ((:clang . ,(list :extraArgs ["-isystem/Library/Developer/CommandLineTools/usr/include/c++/v1" + "-isystem/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include" + "-isystem/usr/local/include"] + :resourceDir (string-trim (shell-command-to-string "clang -print-resource-dir"))))))))) + ;; Eglot specific helper, courtesy of MaskRay + (defun eglot-ccls-inheritance-hierarchy (&optional derived) + "Show inheritance hierarchy for the thing at point. +If DERIVED is non-nil (interactively, with prefix argument), show +the children of class at point." + (interactive "P") + (if-let* ((res (jsonrpc-request + (eglot--current-server-or-lose) + :$ccls/inheritance + (append (eglot--TextDocumentPositionParams) + `(:derived ,(if derived t :json-false)) + '(:levels 100) '(:hierarchy t)))) + (tree (list (cons 0 res)))) + (with-help-window "*ccls inheritance*" + (with-current-buffer standard-output + (while tree + (pcase-let ((`(,depth . ,node) (pop tree))) + (cl-destructuring-bind (&key uri range) (plist-get node :location) + (insert (make-string depth ?\ ) (plist-get node :name) "\n") + (make-text-button (+ (point-at-bol 0) depth) (point-at-eol 0) + 'action (lambda (_arg) + (interactive) + (find-file (eglot--uri-to-path uri)) + (goto-char (car (eglot--range-region range))))) + (cl-loop for child across (plist-get node :children) + do (push (cons (1+ depth) child) tree))))))) + (eglot--error "Hierarchy unavailable")))) (use-package! ccls - :when (featurep! +lsp) + :when (and (featurep! +lsp) (not (featurep! :tools lsp +eglot))) :after lsp :init (after! projectile diff --git a/modules/lang/cc/packages.el b/modules/lang/cc/packages.el index 1beaa6b98..946fad51d 100644 --- a/modules/lang/cc/packages.el +++ b/modules/lang/cc/packages.el @@ -17,7 +17,9 @@ :pin "404cd0694a"))) (if (featurep! +lsp) - (package! ccls :pin "17ec7bb4cf") + (unless (featurep! :tools lsp +eglot) + ;; ccls package is necessary only for lsp-mode. + (package! ccls :pin "17ec7bb4cf")) (when (package! irony :pin "5f75fc0c92") (package! irony-eldoc :pin "0df5831eaa") (when (featurep! :checkers syntax) diff --git a/modules/lang/haskell/packages.el b/modules/lang/haskell/packages.el index 23cdb842d..3792537b6 100644 --- a/modules/lang/haskell/packages.el +++ b/modules/lang/haskell/packages.el @@ -6,6 +6,6 @@ (when (featurep! +dante) (package! dante :pin "4955bc7363") (package! attrap :pin "4cf3e4a162")) -(when (or (featurep! +lsp) +(when (or (and (featurep! +lsp) (not (featurep! :tools lsp +eglot))) (featurep! +ghcide)) (package! lsp-haskell :pin "582fa27c88")) diff --git a/modules/lang/python/config.el b/modules/lang/python/config.el index 8b3073e38..948a26cb6 100644 --- a/modules/lang/python/config.el +++ b/modules/lang/python/config.el @@ -22,7 +22,10 @@ called.") python-indent-guess-indent-offset-verbose nil) (when (featurep! +lsp) - (add-hook 'python-mode-local-vars-hook #'lsp!)) + (add-hook 'python-mode-local-vars-hook #'lsp!) + ;; Use "mspyls" in eglot if in PATH + (when (executable-find "Microsoft.Python.LanguageServer") + (set-eglot-client! 'python-mode '("Microsoft.Python.LanguageServer")))) :config (set-repl-handler! 'python-mode #'+python/open-repl :persist t) (set-docsets! 'python-mode "Python 3" "NumPy" "SciPy") @@ -98,6 +101,7 @@ called.") "Enable `anaconda-mode' if `lsp-mode' is absent and `python-shell-interpreter' is present." (unless (or (bound-and-true-p lsp-mode) + (bound-and-true-p eglot--managed-mode) (bound-and-true-p lsp--buffer-deferred) (not (executable-find python-shell-interpreter))) (anaconda-mode +1)))) @@ -286,7 +290,7 @@ called.") (use-package! lsp-python-ms - :when (featurep! +lsp) + :when (and (featurep! +lsp) (not (featurep! :tools lsp +eglot))) :after lsp-clients :preface (after! python diff --git a/modules/lang/python/packages.el b/modules/lang/python/packages.el index 4788487d6..47d141c4d 100644 --- a/modules/lang/python/packages.el +++ b/modules/lang/python/packages.el @@ -9,7 +9,7 @@ (package! flycheck-cython :pin "ecc4454d35"))) ;; LSP -(when (featurep! +lsp) +(when (and (featurep! +lsp) (not (featurep! :tools lsp +eglot))) (package! lsp-python-ms :pin "5d0c799099")) ;; Programming environment diff --git a/modules/lang/rust/config.el b/modules/lang/rust/config.el index 8cdbf43ea..7bf982684 100644 --- a/modules/lang/rust/config.el +++ b/modules/lang/rust/config.el @@ -27,6 +27,11 @@ (after! rustic-flycheck (add-to-list 'flycheck-checkers 'rustic-clippy))) + (when (featurep! +lsp) + (if (featurep! :tools lsp +eglot) + (setq rustic-lsp-client 'eglot) + (setq rustic-lsp-client 'lsp-mode))) + (map! :map rustic-mode-map :localleader (:prefix ("b" . "build") diff --git a/modules/tools/debugger/config.el b/modules/tools/debugger/config.el index 12e8eeda4..0d582162a 100644 --- a/modules/tools/debugger/config.el +++ b/modules/tools/debugger/config.el @@ -89,7 +89,7 @@ (use-package! dap-mode - :when (featurep! +lsp) + :when (and (featurep! +lsp) (not (featurep! :tools lsp +eglot))) :hook (dap-mode . dap-tooltip-mode) :after lsp-mode :demand t diff --git a/modules/tools/debugger/doctor.el b/modules/tools/debugger/doctor.el new file mode 100644 index 000000000..5583b0efd --- /dev/null +++ b/modules/tools/debugger/doctor.el @@ -0,0 +1,4 @@ +;;; tools/debugger/doctor.el -*- lexical-binding: t; -*- + +(when (and (featurep! +lsp) (featurep! :tools lsp +eglot)) + (warn! "+lsp flag is not compatible with :tools (lsp +eglot). Choose only one of (eglot or dap-mode) please")) diff --git a/modules/tools/lsp/README.org b/modules/tools/lsp/README.org index 8144e8138..6449d54a4 100644 --- a/modules/tools/lsp/README.org +++ b/modules/tools/lsp/README.org @@ -56,10 +56,16 @@ As of this writing, this is the state of LSP support in Doom Emacs: ** Module Flags + =+peek= Use =lsp-ui-peek= when looking up definitions and references with functionality from the =:tools lookup= module. ++ =+eglot= Use [[https://elpa.gnu.org/packages/eglot.html][Eglot]] instead of [[https://github.com/emacs-lsp/lsp-mode][LSP-mode]] to implement the LSP client in + Emacs. ** Plugins + [[https://github.com/emacs-lsp/lsp-mode][lsp-mode]] + [[https://github.com/emacs-lsp/lsp-ui][lsp-ui]] ++ [[https://github.com/emacs-lsp/lsp-ivy][lsp-ivy]] ++ [[https://github.com/emacs-lsp/helm-lsp][helm-lsp]] ++ [[https://github.com/joaotavora/eglot][eglot]] + ** Hacks + ~lsp-mode~ has been modified not to automatically install missing LSP servers. This is done to adhere to our "Your system, your rules" mantra, which insist @@ -75,15 +81,37 @@ You'll find a table that lists available language servers and how to install them [[https://github.com/emacs-lsp/lsp-mode#supported-languages][in the lsp-mode project README]]. The documentation of the module for your targeted language will contain brief instructions as well. +For eglot users, you can see the list of [[https://github.com/joaotavora/eglot/blob/master/README.md#connecting-to-a-server][default servers supported in the README]]. +There is also instructions to add another server easily. + * TODO Features ** LSP-powered project search -When =:completion ivy= or =:completion helm= is active, LSP is used to search a -symbol indexed by the LSP server : +Without the =+eglot= flag, and when =:completion ivy= or =:completion helm= is +active, LSP is used to search a symbol indexed by the LSP server : | Keybind | Description | |-----------+-------------------------------------| | =SPC c j= | Jump to symbol in current workspace | | =SPC c J= | Jump to symbol in any workspace | +** Differences between eglot and lsp-mode +Entering the debate about which one to use would be useless. Doom provides an +easy way to switch out lsp client implementations so you can test for yourself +which one you prefer. + +Mainly, from a code point of view, lsp-mode has a lot of custom code for UI +(=lsp-ui-peek=, =lsp-ui-sideline=, ...), while eglot is more barebones with a +closer integration with "more basic" emacs packages (=eldoc=, =xref=, ...). * TODO Configuration * TODO Troubleshooting +** My language server is not found +Check the entry in the [[../../../docs/faq.org][FAQ]] about "Doom can't find my executables/doesn't inherit +the correct ~PATH~" +** LSP/Eglot is not started automatically in my buffer +Make sure that you added the =+lsp= flag to the language you're using too in +your init.el : +#+BEGIN_SRC diff +:lang +-python ++(python +lsp) +#+END_SRC diff --git a/modules/tools/lsp/autoload/common.el b/modules/tools/lsp/autoload/common.el new file mode 100644 index 000000000..ecb11e99c --- /dev/null +++ b/modules/tools/lsp/autoload/common.el @@ -0,0 +1,9 @@ +;;; tools/lsp/autoload/common.el -*- lexical-binding: t; -*- + +;;;###autodef +(defun lsp! () + "Dispatch to call the currently used lsp client entrypoint" + (interactive) + (if (featurep! +eglot) + (eglot-ensure) + (lsp-deferred))) diff --git a/modules/tools/lsp/autoload/eglot.el b/modules/tools/lsp/autoload/eglot.el new file mode 100644 index 000000000..6ad41005d --- /dev/null +++ b/modules/tools/lsp/autoload/eglot.el @@ -0,0 +1,19 @@ +;;; tools/lsp/autoload/eglot.el -*- lexical-binding: t; -*- +;;;###if (featurep! +eglot) + +;;;###autodef +(defun set-eglot-client! (mode server-call) + "Add SERVER-CALL list as a possible lsp server for given major MODE. + +Example : (set-eglot-client! 'python-mode `(,(concat doom-etc-dir \"lsp/mspyls/Microsoft.Python.LanguageServer\")))" + (when (featurep! +eglot) + (add-to-list 'eglot-server-programs `(,mode . ,server-call)))) + +;;;###autoload +(defun +eglot/documentation-lookup-handler () + "Documentation lookup handler using eglot :document/hover handler. + +Mostly a rewrite of `eglot-help-at-point', which should be used interactively." + (interactive) + (eglot-help-at-point) + (display-buffer eglot--help-buffer)) diff --git a/modules/tools/lsp/autoload.el b/modules/tools/lsp/autoload/lsp-mode.el similarity index 94% rename from modules/tools/lsp/autoload.el rename to modules/tools/lsp/autoload/lsp-mode.el index ec2a90355..0f33155fa 100644 --- a/modules/tools/lsp/autoload.el +++ b/modules/tools/lsp/autoload/lsp-mode.el @@ -1,4 +1,5 @@ -;;; feature/lsp/autoload.el -*- lexical-binding: t; -*- +;;; tools/lsp/autoload/lsp-mode.el -*- lexical-binding: t; -*- +;;;###if (not (featurep! +eglot)) ;;;###autodef (defun set-lsp-priority! (client priority) @@ -9,9 +10,6 @@ priority) (error "No LSP client named %S" client))) -;;;###autodef -(defalias 'lsp! #'lsp-deferred) - ;;;###autoload (defun +lsp/uninstall-server (dir) "Delete a LSP server from `lsp-server-install-dir'." diff --git a/modules/tools/lsp/config.el b/modules/tools/lsp/config.el index a8183e44e..b19805d1a 100644 --- a/modules/tools/lsp/config.el +++ b/modules/tools/lsp/config.el @@ -7,11 +7,34 @@ workspace buffer is closed. This delay prevents premature server shutdown when a user still intends on working on that project after closing the last buffer.") +;; TODO : set eglot-events-buffer-size to nil in doom-debug-mode ;; ;;; Packages +(use-package! eglot + :when (featurep! +eglot) + :init + (setq eglot-sync-connect 1 + eglot-connect-timeout 10 + eglot-autoshutdown t + eglot-send-changes-idle-time 0.5 + ;; NOTE: Do NOT set eglot-auto-display-help-buffer to t. + ;; With popup-rule! :select t, eglot will steal focus from the source code very often. + eglot-auto-display-help-buffer nil) + :config + (set-popup-rule! "^\\*eglot-help" :size 0.35 :quit t :select t) + (when (featurep! :checkers syntax) + ;; Eager loading which is okay-ish since we want eglot to feed flycheck as soon as possible. + (load! "flycheck-eglot.el") + (require 'flycheck-eglot)) + (set-lookup-handlers! 'eglot--managed-mode + :documentation #'+eglot/documentation-lookup-handler + :definition '(xref-find-definitions :async t) + :references '(xref-find-references :async t))) + (use-package! lsp-mode + :unless (featurep! +eglot) :commands lsp-install-server :init (setq lsp-session-file (concat doom-etc-dir "lsp-session")) @@ -123,7 +146,7 @@ This also logs the resolved project root, if found, so we know where we are." (defun +lsp-init-flycheck-or-flymake-h () "Set up flycheck-mode or flymake-mode, depending on `lsp-diagnostic-package'." (pcase lsp-diagnostic-package - ((or :auto 'nil) ; try flycheck, fall back to flymake + ((or :auto 'nil) ; try flycheck, fall back to flymake (let ((lsp-diagnostic-package (if (require 'flycheck nil t) :flycheck :flymake))) (+lsp-init-flycheck-or-flymake-h))) @@ -164,6 +187,7 @@ auto-killed (which is a potentially expensive process)." (use-package! lsp-ui + :unless (featurep! +eglot) :hook (lsp-mode . lsp-ui-mode) :config (setq lsp-ui-doc-max-height 8 @@ -185,10 +209,12 @@ auto-killed (which is a potentially expensive process)." (use-package! helm-lsp + :unless (featurep! +eglot) :when (featurep! :completion helm) :commands helm-lsp-workspace-symbol helm-lsp-global-workspace-symbol) (use-package! lsp-ivy + :unless (featurep! +eglot) :when (featurep! :completion ivy) :commands lsp-ivy-workspace-symbol lsp-ivy-global-workspace-symbol) diff --git a/modules/tools/lsp/doctor.el b/modules/tools/lsp/doctor.el new file mode 100644 index 000000000..aa45cd310 --- /dev/null +++ b/modules/tools/lsp/doctor.el @@ -0,0 +1,3 @@ +;;; tools/lsp/doctor.el -*- lexical-binding: t; -*- + +(assert! (not (and (featurep! +eglot) (featurep! +peek))) "+eglot and +peek flags are not compatible. Peek uses lsp-mode, while Eglot is another package altogether for LSP.") diff --git a/modules/tools/lsp/flycheck-eglot.el b/modules/tools/lsp/flycheck-eglot.el new file mode 100644 index 000000000..b1fcee9ce --- /dev/null +++ b/modules/tools/lsp/flycheck-eglot.el @@ -0,0 +1,57 @@ +;;; flycheck-eglot --- Hacky eglot support in flycheck -*- lexical-binding: t; -*- + +(require 'flycheck) + +;;; Code: +(defun flycheck-eglot--start (checker callback) + "Clean up errors when done. + +CHECKER is the checker (eglot). +CALLBACK is the function that we need to call when we are done, on all the errors." + (cl-labels + ((flymake-diag->flycheck-err + (diag) + (with-current-buffer (flymake--diag-buffer diag) + (flycheck-error-new-at-pos + (flymake--diag-beg diag) + (pcase (flymake--diag-type diag) + ('eglot-note 'info) + ('eglot-warning 'warning) + ('eglot-error 'error) + (_ (error "Unknown diagnostic type, %S" diag))) + (flymake--diag-text diag) + :end-pos (flymake--diag-end diag) + :checker checker + :buffer (current-buffer) + :filename (buffer-file-name))))) + ;; NOTE: Setting up eglot to automatically create flycheck errors for the buffer. + (eglot-flymake-backend (lambda (flymake-diags &rest _) + (funcall callback 'finished (mapcar #'flymake-diag->flycheck-err flymake-diags)))) + ;; NOTE: Forcefully trigger a check in the buffer (function name is confusing) + (flycheck-buffer))) + +(defun flycheck-eglot--available-p () + (bound-and-true-p eglot--managed-mode)) + +(flycheck-define-generic-checker 'eglot + "Report `eglot' diagnostics using `flycheck'." + :start #'flycheck-eglot--start + :predicate #'flycheck-eglot--available-p + :modes '(prog-mode text-mode)) + +(push 'eglot flycheck-checkers) + +(defun +doom/eglot-prefer-flycheck-h () + (when eglot--managed-mode + (when-let ((current-checker (flycheck-get-checker-for-buffer))) + (unless (equal current-checker 'eglot) + (flycheck-add-next-checker 'eglot current-checker))) + (flycheck-add-mode 'eglot major-mode) + (flycheck-mode 1) + (flymake-mode -1))) + +(add-hook 'eglot--managed-mode-hook #'+doom/eglot-prefer-flycheck-h) + +(provide 'flycheck-eglot) +;;; flycheck-eglot.el ends here + diff --git a/modules/tools/lsp/packages.el b/modules/tools/lsp/packages.el index d0ed445dd..b8117fa61 100644 --- a/modules/tools/lsp/packages.el +++ b/modules/tools/lsp/packages.el @@ -1,9 +1,12 @@ ;; -*- no-byte-compile: t; -*- ;;; tools/lsp/packages.el -(package! lsp-mode :pin "81d62d581b21d847783831e6e5ca9d3c63fe9a4d") -(package! lsp-ui :pin "271b47cb33f11915295911f7cf8575f8a82a5e1c") -(when (featurep! :completion ivy) - (package! lsp-ivy :pin "dce58b5509271bbedb53ba9d0278dcb563a43977")) -(when (featurep! :completion helm) - (package! helm-lsp :pin "6b5ce182d7c94c62b55b8f7d0c7e643b2c30e560")) +(if (featurep! +eglot) + (package! eglot :pin "d99a4478a9") + (progn + (package! lsp-mode :pin "81d62d581b21d847783831e6e5ca9d3c63fe9a4d") + (package! lsp-ui :pin "271b47cb33f11915295911f7cf8575f8a82a5e1c") + (when (featurep! :completion ivy) + (package! lsp-ivy :pin "dce58b5509271bbedb53ba9d0278dcb563a43977")) + (when (featurep! :completion helm) + (package! helm-lsp :pin "6b5ce182d7c94c62b55b8f7d0c7e643b2c30e560"))))