;;; helm-lsp.el --- LSP helm integration -*- lexical-binding: t; -*- ;; Copyright (C) 2018 Ivan Yonchovski ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;; Author: Ivan Yonchovski ;; Keywords: languages, debug ;; URL: https://github.com/yyoncho/helm-lsp ;; Package-Requires: ((emacs "25.1") (dash "2.14.1") (lsp-mode "5.0") (helm "2.0")) ;; Version: 0.2 ;;; Commentary: ;; `helm' for lsp function. ;;; Code: (require 'helm) (require 'helm-imenu) (require 'dash) (require 'lsp-mode) (defvar helm-lsp-symbols-request-id nil) (defvar helm-lsp-symbols-result-p nil) (defvar helm-lsp-symbols-result nil) (defgroup helm-lsp nil "`helm-lsp' group." :group 'lsp-mode :tag "Language Server") (lsp-defun helm-lsp-workspace-symbol-action ((&SymbolInformation :location)) "Action for helm workspace symbol. CANDIDATE is the selected item in the helm menu." (lsp-goto-location location)) (defface helm-lsp-container-face '((t :height 0.8 :inherit shadow)) "The face used for code lens overlays." :group 'helm-lsp) (defcustom helm-lsp-treemacs-icons t "If non-nil, use `lsp-treemacs' icons." :group 'helm-lsp :type 'boolean) (defun helm-lsp--extract-file-name (uri) "Extract file name from URI." (propertize (if (string= "jdt" (-> uri url-unhex-string url-generic-parse-url url-type)) (cl-second (s-match ".*\(\\(.*\\)" uri)) (f-filename uri)) 'face 'helm-lsp-container-face)) (defun helm-lsp--get-icon (kind) "Get the icon by KIND." (require 'lsp-treemacs) (ht-get (treemacs-theme->gui-icons (treemacs--find-theme lsp-treemacs-theme)) (lsp-treemacs-symbol-kind->icon kind))) (defun helm-lsp--workspace-symbol (workspaces name input) "Search against WORKSPACES NAME with default INPUT." (setq helm-lsp-symbols-result nil) (if workspaces (with-lsp-workspaces workspaces (helm :sources (helm-build-sync-source name :candidates (lambda () (if helm-lsp-symbols-result-p helm-lsp-symbols-result (with-lsp-workspaces workspaces (-let (((request &as &plist :id request-id) )) (setq helm-lsp-symbols-request-id request-id) (lsp-request-async "workspace/symbol" (list :query helm-pattern) (lambda (candidates) (setq helm-lsp-symbols-request-id nil) (and helm-alive-p (let ((helm-lsp-symbols-result-p t)) (setq helm-lsp-symbols-result candidates) (helm-update)))) :mode 'detached :cancel-token :workspace-symbols) helm-lsp-symbols-result)))) :action 'helm-lsp-workspace-symbol-action :volatile t :fuzzy-match t :match (-const t) :keymap helm-map :candidate-transformer (lambda (candidates) (-map (-lambda ((candidate &as &SymbolInformation :container-name? :name :kind :location (&Location :uri))) (let ((type (or (alist-get kind lsp-symbol-kinds) "Unknown"))) (cons (if (and (featurep 'lsp-treemacs) helm-lsp-treemacs-icons) (concat (or (helm-lsp--get-icon kind) (helm-lsp--get-icon 'fallback)) (if (s-blank? container-name?) name (concat name " " (propertize container-name? 'face 'helm-lsp-container-face))) (propertize " · " 'face 'success) (helm-lsp--extract-file-name uri)) (concat (if (s-blank? container-name?) name (concat name " " (propertize container-name? 'face 'helm-lsp-container-face) " -" )) " " (propertize (concat "(" type ")") 'face 'font-lock-type-face) (propertize " · " 'face 'success) (helm-lsp--extract-file-name uri))) candidate))) (-take helm-candidate-number-limit candidates))) :candidate-number-limit nil :requires-pattern 0) :input input)) (user-error "No LSP workspace active"))) ;;;###autoload (defun helm-lsp-workspace-symbol (arg) "`helm' for lsp workspace/symbol. When called with prefix ARG the default selection will be symbol at point." (interactive "P") (helm-lsp--workspace-symbol (or (lsp-workspaces) (gethash (lsp-workspace-root default-directory) (lsp-session-folder->servers (lsp-session)))) "Workspace symbol" (when arg (thing-at-point 'symbol)))) ;;;###autoload (defun helm-lsp-global-workspace-symbol (arg) "`helm' for lsp workspace/symbol for all of the current workspaces. When called with prefix ARG the default selection will be symbol at point." (interactive "P") (helm-lsp--workspace-symbol (-uniq (-flatten (ht-values (lsp-session-folder->servers (lsp-session))))) "Global workspace symbols" (when arg (thing-at-point 'symbol)))) ;;;###autoload (defun helm-lsp-code-actions() "Show lsp code actions using helm." (interactive) (let ((actions (lsp-code-actions-at-point))) (cond ((seq-empty-p actions) (signal 'lsp-no-code-actions nil)) ((and (eq (seq-length actions) 1) lsp-auto-execute-action) (lsp-execute-code-action (lsp-seq-first actions))) (t (helm :sources (helm-build-sync-source "Code Actions" :candidates actions :candidate-transformer (lambda (candidates) (-map (-lambda ((candidate &as &CodeAction :title)) (list title :data candidate)) candidates)) :action '(("Execute code action" . (lambda(candidate) (lsp-execute-code-action (plist-get candidate :data))))))))))) ;; helm projects (with-eval-after-load 'helm-projectile (defvar helm-lsp-source-projects (helm-build-sync-source "LSP projects" :candidates (lambda () (lsp-session-folders (lsp-session))) :fuzzy-match helm-projectile-fuzzy-match :keymap helm-projectile-projects-map :mode-line helm-read-file-name-mode-line-string :action 'helm-source-projectile-projects-actions) "Helm source for known LSP projects.") (defun helm-lsp-switch-project (&optional arg) "Use projectile with Helm for finding files in project With a prefix ARG invalidates the cache first." (interactive "P") (let ((helm-ff-transformer-show-only-basename nil) (helm-boring-file-regexp-list nil)) (helm :sources 'helm-lsp-source-projects :buffer (concat "*helm projectile: " (projectile-project-name) "*") :truncate-lines helm-projectile-truncate-lines :prompt (projectile-prepend-project-name "Switch to LSP project: "))))) ;; helm diagnostics (defconst helm-lsp--diag-mapping `((,lsp/diagnostic-severity-error . error) (,lsp/diagnostic-severity-warning . warning) (,lsp/diagnostic-severity-information . info) (,lsp/diagnostic-severity-hint . info))) (lsp-defun helm-lsp--diag-matched (file (&Diagnostic :message :source? :severity? :range (&Range :start (&Position :line :character))) tokens) (-all? (lambda (token) (cl-case (aref token 0) (?# (s-matches? (substring token 1) file)) (?* (s-contains? (substring token 1) (symbol-name (alist-get severity? helm-lsp--diag-mapping)))) (t (s-contains? token message)))) tokens)) (lsp-defun helm-lsp-jump-to-error ((file start)) "Go to selected symbol" (find-file file) (goto-char (lsp--position-to-point start))) (lsp-defun helm-lsp-quick-fix ((file start)) "Go to selected symbol and fix the action." (find-file file) (goto-char (lsp--position-to-point start)) (call-interactively #'lsp-execute-code-action)) (defface helm-lsp-diag-error '((t :inherit error)) "Face used for corresponding diag error level." :group 'lsp-faces) (defface helm-lsp-diag-info '((t :inherit success)) "Face used for corresponding diag error level." :group 'lsp-faces) (defface helm-lsp-diag-warning '((t :inherit warning)) "Face used for corresponding diag error level." :group 'lsp-faces) (defcustom helm-lsp-diag-face-map `((,lsp/diagnostic-severity-error . helm-lsp-diag-error) (,lsp/diagnostic-severity-warning . helm-lsp-diag-warning) (,lsp/diagnostic-severity-information . helm-lsp-diag-info) (,lsp/diagnostic-severity-hint . helm-lsp-diag-info)) "Alist diagnostics to face." :type 'alist) (defun helm-lsp--diagnostics-transform (candidates) (let ((tokens (helm-mm-split-pattern helm-pattern))) (->> candidates (-keep (-lambda ((full-path file (diag &as &Diagnostic :message :source? :severity? :range (&Range :start (start &as &Position :line :character)) ))) (when (helm-lsp--diag-matched full-path diag tokens) (list (format "%s%s %s %s %s%s" (if (fboundp 'lsp-treemacs-get-icon) (lsp-treemacs-get-icon (alist-get severity? helm-lsp--diag-mapping)) (propertize (format "[%s] " (alist-get severity? helm-lsp--diag-mapping)) 'face (alist-get severity? helm-lsp-diag-face-map))) (propertize (format "[%s]" source?) 'face 'lsp-details-face) source? message (propertize file 'face 'lsp-details-face) (propertize (format ":%s:%s" line character) 'face 'lsp-details-face)) full-path start)))) (-sort (-lambda ((full-path-1 _ (&Diagnostic :range (&Range? :start (&Position? :line l1 :character c1)))) (full-path-2 _ (&Diagnostic :range (&Range? :start (&Position? :line l2 :character c2))))) (if (string= full-path-1 full-path-2) (cond ((not l1) t) ((not l2) nil) (t (if (eq l1 l2) (< c1 c2) (< l1 l2)))) (string< full-path-1 full-path-2))))))) ;;;###autoload (defun helm-lsp-diagnostics (arg) "Diagnostics using `helm'" (interactive "P") (if (get-buffer "*helm-lsp-diagnostics*") (progn (run-with-timer 0 nil #'helm-update) (helm-resume "*helm-lsp-diagnostics*")) (helm :sources (helm-build-sync-source "Diagnostics" :mode-line (list "Diagnostics(s)") :candidates (lambda () (->> (lsp-diagnostics) (ht-map (lambda (file v) (-map (-partial #'list file (if-let ((wks (lsp-workspace-root file))) (f-relative file wks) file)) v))) (apply #'append))) :action '(("Goto diagnostic" . helm-lsp-jump-to-error) ("Quick fix" . helm-lsp-quick-fix)) :persistent-action #'helm-lsp-jump-to-error :match (-const t) :volatile t :candidate-transformer #'helm-lsp--diagnostics-transform) :candidate-number-limit nil :buffer "*helm-lsp-diagnostics*"))) (provide 'helm-lsp) ;;; helm-lsp.el ends here