;;; lsp-metals-treeview.el --- LSP Scala Metals Treeview   -*- lexical-binding: t; -*-

;; Copyright (C) 2019 Darren Syzling <dsyzling@gmail.com>, Evgeny Kurnevsky <kurnevsky@gmail.com>

;; Author: Darren Syzling <dsyzling@gmail.com>
;;         Evgeny Kurnevsky <kurnevsky@gmail.com>
;; Keywords: languages, extensions
;; URL: https://github.com/emacs-lsp/lsp-metals

;; 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 <https://www.gnu.org/licenses/>.

;;; Commentary:

;; lsp-metals treeview ui client - handles a treeview for project tree,
;; compilation tree etc.
;; See the Metals treeview provider spec for further details:
;;  https://scalameta.org/metals/docs/editors/tree-view-protocol.html
;;
;; Current treeview interaction is:
;;   tab  key to expand/collapse nodes which is default treemacs behaviour.
;;   ret  will execute the command associated with the current node via Metals.
;;        Note you need -Dmetals.execute-client-command enabled for this to work
;;        and may require you to upgrade Metals post 0.7 for Emacs.
;;
;;   mouse left double click - will execute the command on a node.
;;
;; Metals allows classes to be expanded and the action executed on the same
;; node - metals.goto (goto definition) we can't therefore use return to
;; expand/collapse and execute actions.  The existing implementation provides
;; a simple starting point to test the treeview with metals and we can evolve
;; to a Hydra like interface to provide a richer keyboard experience in future.
;;

;;; Code:

(require 'ht)
(require 'json)
(require 'dash)
(require 'f)
(require 'seq)
(require 'pcase)
(require 'treemacs-treelib)
(require 'lsp-mode)
(require 'lsp-treemacs)
(require 'lsp-metals-protocol)

(dolist (method '("metals/treeViewChildren"
                  "metals/treeViewVisibilityDidChange"
                  "metals/treeViewNodeCollapseDidChange"
                  "metals/treeViewReveal"))
  (add-to-list 'lsp-method-requirements
               `(,method :check-command (lambda (workspace)
                                          (eq (lsp--workspace-server-id workspace) 'metals)))
               t))

(defcustom lsp-metals-treeview-logging nil
  "If non nil log treeview trace/debug messages to the `lsp-log' for debugging."
  :group 'lsp-metals-treeview
  :type 'boolean)

(defcustom lsp-metals-treeview-workspace-switch-delay 0.2
  "Delay in seconds for switching treeview between workspaces.
The delay occurs after `buffer-list-update-hook' is called before
triggering a switch of treeview when navigating between buffers in
different workspaces."
  :group 'lsp-metals-treeview
  :type 'float)

(defcustom lsp-metals-treeview-views '("metalsPackages"
                                       "metalsBuild"
                                       "metalsCompile"
                                       "metalsHelp")
  "List of views to display."
  :group 'lsp-metals-treeview
  :type '(repeat string)
  :package-version '(lsp-metals . "1.2"))

(defcustom lsp-metals-treeview-theme
  (pcase (frame-parameter nil 'background-mode)
    ('dark 'Metals-dark)
    (_ 'Metals-light))
  "The theme for treeview icons."
  :group 'lsp-metals-treeview
  :type '(choice
           (const :tag "Light" Metals-light)
           (const :tag "Dark" Metals-dark))
  :package-version '(lsp-metals . "1.2"))

(defvar-local lsp-metals-treeview--current-workspace nil
  "Associate lsp workspace with the metals treeview buffer.
Needed to make async calls to the lsp server from treemacs buffers.")

(defvar-local lsp-metals-treeview--view-id nil
  "Metals treeview id associated with the treeview buffer.")

(defconst lsp-metals-treeview--icon-dir "icons"
  "Directory containing Metals treeview icon theme.
Directory is relative to lsp-metals.")

(defconst lsp-metals-treeview--buffer-prefix " *Metals"
  "Prefix for all Metals treeview buffers.
Note the space prefix which hides the buffers within the buffer
list in Emacs.")

;; Root directory of our lisp files so that we can find icons
;; relative to installation.
(defconst lsp-metals-treeview--dir
  (-> (if load-file-name
          (file-name-directory load-file-name)
        default-directory)
      (expand-file-name))
  "The directory lsp-metals-treeview.el is stored in.")

(defconst lsp-metals-treeview--buffers-key "metals-treeview-buffers"
  "Metadata key to store treeview buffers within workspace.")

(defconst lsp-metals-treeview--metals-server-id 'metals
  "Server id metals lsp client should be registered from within lsp-metals.")

;;
;; Treemacs doesn't support a unique key - :-key-form isn't actually defined as
;; being unique and you cannot search by this key - only by path. Since Metals
;; sends us nodeUri unique keys we need someway of mapping nodeUris to
;; treemacs paths - so we can use treemacs-find-node.
;;
(defvar-local lsp-metals-treeview--treemacs-node-index (make-hash-table :test 'equal))

(defvar lsp-metals-treeview--active-view-workspace nil
  "Workspace associated with the active treeview instance.
When the treeview is displayed and visible this variable
will hold the workspace.")

(defun lsp-metals-treeview--position (slot)
  "Side window position of Metals treeview with the given SLOT.
Uses defaults for treemacs position and width."
  `((side . ,treemacs-position)
    (slot . ,slot)
    (window-width . ,treemacs-width)))

(defun lsp-metals-treeview--buffer-changed ()
  "Active buffer has changed check if current treeview is valid.
When the buffer is switched check to see if a treeview
is currently being displayed and whether we need to show
an alternative workspace's treeview."
  (with-current-buffer (current-buffer)
    (when (and (eq major-mode 'scala-mode)
               (lsp-find-workspace lsp-metals-treeview--metals-server-id nil)
               lsp-metals-treeview--active-view-workspace
               (not (member lsp-metals-treeview--active-view-workspace
                            (lsp-workspaces))))

      ;; hide current treeview and show new window associated with
      ;; the current workspace of file in buffer.
      (lsp-metals-treeview--hide-window lsp-metals-treeview--active-view-workspace)
      (lsp-metals-treeview--show-window (car (lsp-workspaces))))))

(defun lsp-metals-treeview--buffer-list-update ()
  "Active buffer changed check if treeview needs to be changed.
If the user switches buffers in different workspaces we need to
swap the treeview to show the new workspace's treeview."
  (run-with-idle-timer lsp-metals-treeview-workspace-switch-delay
                       nil
                       #'lsp-metals-treeview--buffer-changed))

(defun lsp-metals-treeview--add-workspace-switch-hook ()
  "Add hook to swap treeviews between workspace buffers.
Add a `buffer-list-update-hook' to hide/show the active treeview
- if currently displayed - when the user switches buffers that are
within another workspace."
  (add-hook 'buffer-list-update-hook
            #'lsp-metals-treeview--buffer-list-update))

(defun lsp-metals-treeview--remove-workspace-switch-hook ()
  "Remove the hook to swap treeviews between workspace buffers.
Remove the `buffer-list-update-hook' which deals with switching
treeview when the active buffer switches between different
workspaces."
  (remove-hook 'buffer-list-update-hook
               #'lsp-metals-treeview--buffer-list-update))

(defun lsp-metals-treeview--log (format &rest args)
  "Log treeview tracing/debug messages to the lsp log.
Use the FORMAT formatting string and ARGS arguments to format
the message and parameters."
  (when lsp-metals-treeview-logging
    (apply #'lsp-log format args)))

(defun lsp-metals-treeview--add-buffer (workspace view-id buffer)
  "Add BUFFER with key VIEW-ID to treeview buffers stored in the WORKSPACE."
  (push (cons view-id buffer) (ht-get (lsp--workspace-metadata workspace) lsp-metals-treeview--buffers-key)))

(defun lsp-metals-treeview--remove-buffers (workspace)
  "Clear treeview buffers stored in the WORKSPACE."
  (setf (ht-get (lsp--workspace-metadata workspace) lsp-metals-treeview--buffers-key) nil))

(defun lsp-metals-treeview--get-buffers (workspace)
  "Return buffers stored in the WORKSPACE."
  (-map #'cdr (ht-get (lsp--workspace-metadata workspace) lsp-metals-treeview--buffers-key)))

(defun lsp-metals-treeview--get-buffer-by-id (workspace view-id)
  "Return buffer with key VIEW-ID stored in the WORKSPACE."
  (alist-get view-id (ht-get (lsp--workspace-metadata workspace) lsp-metals-treeview--buffers-key) nil nil #'equal))

(defun lsp-metals-treeview--view-name (view-id)
  "Return a view name from the VIEW-ID."
  (replace-regexp-in-string "metals" "" view-id))

(defun lsp-metals-treeview--buffer-name (workspace view-id)
  "Return buffer name of the treeview from WORKSPACE and VIEW-ID."
  (format "%s %s %s*"
          lsp-metals-treeview--buffer-prefix
          (lsp-metals-treeview--view-name view-id)
          (file-name-nondirectory
           (directory-file-name (lsp--workspace-root workspace)))))

(defun lsp-metals-treeview--hide-window (&optional workspace)
  "Hide the Metals treeview window associated with the WORKSPACE.
The window will be deleted but the treeview buffers will still
be live in the background."
  (interactive)
  (-when-let (cur-workspace (or workspace lsp-metals-treeview--current-workspace))
    (-map (lambda (buffer)
            ;; Notify Metals that visibility of the view has changed
            (with-current-buffer buffer
              (lsp-metals-treeview--send-visibility-did-change
               cur-workspace lsp-metals-treeview--view-id nil))
            (delete-window (get-buffer-window buffer)))
          (lsp-metals-treeview--get-buffers cur-workspace))
    (setq lsp-metals-treeview--active-view-workspace nil)
    ;; Only keep this treeview switching hook live when absolutely necessary
    (lsp-metals-treeview--remove-workspace-switch-hook)))

(defun lsp-metals-treeview--get-visible-buffers ()
  "Retrieve buffers associated with the current selected frame.
Check to see if any of these buffers are metals treeview buffers and
if so return the buffers."
  ;; retrieve any treeview buffers that are visible
  (->> (window-list (selected-frame))
       (-keep (lambda (window)
               (let ((buffer (window-buffer window)))
                 (when (s-starts-with? lsp-metals-treeview--buffer-prefix
                                       (buffer-name buffer))
                   buffer))))))

(defun lsp-metals-treeview--visible? (workspace)
  "Is the metals treeview associated with the WORKSPACE currently visible?"
  (-when-let* ((visible-buffers (lsp-metals-treeview--get-visible-buffers))
               (workspace-buffers (lsp-metals-treeview--get-buffers workspace)))
    (equal visible-buffers workspace-buffers)))

(defun lsp-metals-treeview--exists? (workspace)
  "Does a Metals Treeview exist for the WORKSPACE?
The treeview may not be visible but still exists in the background."
  (-when-let (buffers (lsp-metals-treeview--get-buffers workspace))
    (-all-p 'buffer-live-p buffers)))

(defun lsp-metals-treeview--hidden? (workspace)
  "Does the metals treeview associated with WORKSPACE exist but not visible?"
  (and (lsp-metals-treeview--exists? workspace)
       (not (lsp-metals-treeview--visible? workspace))))

(defun lsp-metals-treeview--get-visibility (workspace)
  "Return visibility status of metals treeview associated with WORKSPACE.
Return `visible', `hidden', `none' depending on state of treeview."
  (cond
   ((lsp-metals-treeview--visible? workspace) 'visible)
   ((lsp-metals-treeview--exists? workspace)  'hidden)
   (t 'none)))

(defun lsp-metals-treeview--show-window (workspace &optional select-window?)
  "Show metals treeview window associated with WORKSPACE.
Optionally select the window based on SELECT-WINDOW? being True.
If the treeview window is hidden or not visible (not created)
then show the window."
  (let ((visibility (lsp-metals-treeview--get-visibility workspace)))
    (when (or (eq 'hidden visibility) (eq 'none visibility))
      (lsp-metals-treeview--show-views workspace 0 select-window?))))

(defun lsp-metals-treeview--delete-window (&optional workspace workspace-shutdown?)
  "Delete the metals treeview window associated with the WORKSPACE.
If WORKSPACE is not provided the current treeview buffer local variable
WORKSPACE will be used.  This function is also called from an lsp hook
which will be called when the workspace is shutdown - in this case we
won't notify Metals of view being hidden if WORKSPACE-SHUTDOWN? is
t."
  (let ((cur-workspace (or workspace lsp-metals-treeview--current-workspace)))
    (-map (lambda (treeview-buffer)
            (switch-to-buffer treeview-buffer)
            ;; Tell metals the view is no longer visible but only if
            ;; the workspace isn't in the process of shutting down or
            ;; not initialised.
            (when (and lsp-metals-treeview--view-id
                       (not workspace-shutdown?)
                       (equal 'initialized (lsp--workspace-status cur-workspace)))
              (lsp-metals-treeview--send-visibility-did-change
               lsp-metals-treeview--current-workspace
               lsp-metals-treeview--view-id
               nil))
            (kill-buffer treeview-buffer))
          (lsp-metals-treeview--get-buffers cur-workspace))
    (lsp-metals-treeview--remove-buffers cur-workspace)
    (setq lsp-metals-treeview--active-view-workspace nil)
    ;; Only keep this treeview switching hook live when absolutely necessary.
    (lsp-metals-treeview--remove-workspace-switch-hook)
    (remove-hook 'lsp-after-uninitialized-functions #'lsp-metals-treeview--delete-window)))

(defun lsp-metals-treeview--on-workspace-shutdown (workspace)
  "Handler for lsp WORKSPACE shutdown.
Ensure we close our treeview windows/buffers.  Under this scenario we
shouldn't contact Metals to update view visibility status, so we pass
through workspace-shutdown true so that the `delete-window' function has
the context of the window closing."
  (lsp-metals-treeview--delete-window workspace t))

;;
;; Minor mode for metals treeview window and keymap to control
;; functions such as closing window.
;;

(defvar lsp-metals-treeview-mode-map
  (let ((m (make-sparse-keymap)))
    (define-key m (kbd "q") #'lsp-metals-treeview--hide-window)
    m)
  "Keymap for `lsp-metals-treeview-mode'.")

(define-minor-mode lsp-metals-treeview-mode
  "LSP Metals Treeview minor mode."
  :keymap lsp-metals-treeview-mode-map
  :group 'lsp-metals-treeview)


(defun lsp-metals-treeview--show-view (workspace view-id position)
  "Show or create the side window and treeview.
The window will be created for the Metals VIEW-ID within the current WORKSPACE.
The window will be positioned as a side window by POSITION and is of the
form `((side left))'."
  (let ((buffer-name (lsp-metals-treeview--buffer-name workspace
                                                       (lsp-metals-treeview--view-name view-id))))
    ;; When opening or refreshing the view do temporarily switch focus but restore
    ;; after window has been created. User will then not be diverted away from their
    ;; current focus..
    (-if-let (buffer (get-buffer buffer-name))
        (with-selected-window (display-buffer-in-side-window buffer position)
          ;; update the root of the tree with the view.
          (lsp-metals-treeview--log "Refreshing tree %s" view-id)
          (treemacs-update-node '(metals-root) t)
          (set-window-dedicated-p (selected-window) t)
          ;; When closing other windows after splitting, prevent our treeview closing.
          (set-window-parameter (selected-window) 'no-delete-other-windows t))

      (let* ((buffer (get-buffer-create buffer-name))
             (window (display-buffer-in-side-window buffer position)))

        (with-lsp-workspace workspace
          (with-selected-window window
            (set-window-dedicated-p window t)
            (treemacs-initialize metals-root
              :and-do (progn
                        (setq-local lsp-metals-treeview--current-workspace workspace)
                        (setq-local lsp-metals-treeview--view-id view-id)))

            (setq-local mode-line-format (concat
                                          (treemacs-get-icon-value 'root nil "Metals")
                                          (lsp-metals-treeview--view-name view-id)))

            ;; Add buffer to list of treeview buffers associated with this workspace.
            (lsp-metals-treeview--add-buffer workspace view-id buffer)

            ;; When closing other windows after splitting, prevent our treeview closing.
            (set-window-parameter window 'no-delete-other-windows t)
            (lsp-metals-treeview-mode 1)

            (setq-local treemacs-default-visit-action 'treemacs-RET-action)
            (setq-local treemacs-doubleclick-actions-config '((treemacs-metals-node-open . treemacs-RET-action)
                                                              (treemacs-metals-node-closed . treemacs-RET-action)))))))))


(defun lsp-metals-treeview--display-views (workspace views slot)
  "Recursive function to display each view in VIEWS.
The views will be associated with the WORKSPACE and displayed in the side
window based based on an increasing SLOT number position."
  (when-let ((view-id (car views)))
    (lsp-metals-treeview--show-view workspace
                           view-id
                           (lsp-metals-treeview--position slot))
    (lsp-metals-treeview--send-visibility-did-change workspace view-id t)
    (lsp-metals-treeview--display-views workspace (cdr views) (+ 1 slot))))

(defun lsp-metals-treeview--select-window (workspace)
  "Switch focus to the treeview window in the given WORKSPACE.
Select the first view/buffer in the treeview window."
  (select-window (get-buffer-window
                  (car (lsp-metals-treeview--get-buffers workspace)))))

(defun lsp-metals-treeview--show-views (workspace slot &optional select-treeview-window)
  "Display each metals view in our sidebar treeview window.
Views are displayed for this WORKSPACE.  SLOT is a numeric position starting
from 0 where the treeview will be positioned relative to the others.  when
SELECT-TREEVIEW-WINDOW is t the treeview window will be selected and have
focus."
  (lsp-metals-treeview--display-views workspace lsp-metals-treeview-views slot)

  (when select-treeview-window
    (lsp-metals-treeview--select-window workspace))

  (setq lsp-metals-treeview--active-view-workspace workspace)

  ;; When user switches between files in workspaces automatically switch
  ;; the treeview to the appropriate one.
  (lsp-metals-treeview--add-workspace-switch-hook)

  ;; Add hook to close our treeview when the workspace is shutdown.
  (add-hook 'lsp-after-uninitialized-functions #'lsp-metals-treeview--on-workspace-shutdown))


(defun lsp-metals-treeview--cache-add-nodes (metals-nodes current-treemacs-node)
  "Build an index of treemacs nodes nodeUri -> treemacs path.
We can use this to find nodes within the tree based on nodeUri which
Metals will send us.  METALS-NODES contains a list of new nodes added to
the tree to be displayed, CURRENT-TREEMACS-NODE is the paren to the new
nodes."
  (let ((parent-path (treemacs-button-get current-treemacs-node :path)))
    (-map (lambda (metals-node)
            (-when-let ((&TreeViewNode :node-uri?) metals-node)
              (ht-set lsp-metals-treeview--treemacs-node-index
                      node-uri?
                      (append parent-path (list node-uri?)))))
          metals-nodes)))

(defun lsp-metals-treeview--find-node (node-uri)
  "Find treemacs node based on NODE-URI via our local index.
If the node cannot be found in the tree make sure we cleanup the cache
and remove it."
  (-if-let* ((path (ht-get lsp-metals-treeview--treemacs-node-index node-uri))
             (found-node (treemacs-find-node path)))
      found-node
    ;; Otherwise remove node form cache it's no longer in the tree.
    (ht-remove lsp-metals-treeview--treemacs-node-index node-uri)
    nil))


(lsp-defun lsp-metals-treeview--did-change (workspace (&TreeViewDidChangeParams :nodes))
  "Metals treeview changed notification.
Nodes that have been changed will be provided within the
PARAMS message with their viewIds.  WORKSPACE will be the current
workspace of the project."
  (lsp-metals-treeview--log "In lsp-metals-treeview--did-change %s\n%s"
                            (lsp--workspace-root workspace)
                            (lsp--json-serialize nodes))

  (mapc (lambda (node)
          (-when-let* (((&TreeViewNode :view-id :label) node)
                       (buffer (lsp-metals-treeview--get-buffer-by-id workspace view-id)))
            (with-current-buffer buffer
              (-if-let ((&TreeViewNode :node-uri?) node)
                  (-if-let (tree-node (lsp-metals-treeview--find-node node-uri?))
                      (progn
                        ;; replace label in our node attached to the tree node.
                        (lsp:set-tree-view-node-label (treemacs-button-get tree-node :node) label)

                        ;; Currently the only way to re-render the label of an item is
                        ;; for the parent to call render-node on its children. So
                        ;; we update the parent of the node we're changing.
                        ;; An enhancement to treemacs is in the works where  the label
                        ;; can be updated directly.
                        (treemacs-update-node (treemacs-button-get tree-node :parent) nil))
                    (lsp-metals-treeview--log "Failed to find node in treeview"))
                (treemacs-update-node '(metals-root) t)))))
        nodes))


(defun lsp-metals-treeview--send-treeview-children (view-id &optional node-uri)
  "Query children in the view given by VIEW-ID.
An optional NODE-URI can be used to query children of a specific node
within the view.  This call is synchronous and will return the response
from the call to metas/treeViewChildren.  Under the hood LSP-REQUEST will
send the request asynchronously and wait for the response."
  (lsp-metals-treeview--log "Sending metals/treeViewChildren")
  (lsp-request "metals/treeViewChildren"
               (append `(:viewId ,view-id)
                       (if node-uri `(:nodeUri ,node-uri) nil))))


(defun lsp-metals-treeview--send-visibility-did-change (workspace view-id visible?)
  "Send metals/treeViewVisibilityDidChange when views are shown/hidden.
We need to keep Metals informed when views are shown/hidden so it can optimise
the notification messages it sends us.  WORKSPACE is the current lsp workspace,
VIEW-ID is the view for which the visibility has changed described by the
boolean value VISIBLE - t or nil."
  (lsp-metals-treeview--log "view visibility changed %s %s" view-id visible?)
  (let ((params (list :viewId view-id
                      :visible visible?)))
    (with-lsp-workspace workspace
        (lsp-request-async "metals/treeViewVisibilityDidChange" params
                           (lambda (response)
                             (lsp-metals-treeview--log (lsp--json-serialize response)))
                           :mode 'detached))))

(defun lsp-metals-treeview--send-node-collapse-did-change (workspace view-id node-uri collapsed?)
  "Send metals/treeViewNodeCollapseDidChange when a node has collapsed/expanded.
WORKSPACE is the current workspace, VIEW-ID the id of the view containing the
node with NODE-URI which has been collapsed or expanded based on the boolean
COLLAPSED? either t or nil."
  (lsp-metals-treeview--log "sending metals/treeViewNodeCollapseDidChange viewId %s nodeUri %s collapsed? %s"
                            view-id node-uri collapsed?)
  (let ((params (list :viewId view-id
                      :nodeUri node-uri
                      :collapsed (if collapsed?
                                     t
                                   json-false))))
    (with-lsp-workspace workspace
      (lsp-request-async "metals/treeViewNodeCollapseDidChange" params
                         (lambda (response)
                           (lsp-metals-treeview--log "metals/treeViewNodeCollapseDidChange response:\n %s"
                                                     (lsp--json-serialize response)))
                         :mode 'detached))))

(defun lsp-metals-treeview--get-children (view-id &optional node-uri)
  "Retrieve children of the view given by the VIEW-ID and optionally the node.
Children of the view can be returned and optionally by specifying a NODE-URI
this function will return the node's children.  Without a NODE-URI the top
level child items will be returned for the view.  Returns a list of nodes
with values converted from json to hash tables."
  (with-lsp-workspace lsp-metals-treeview--current-workspace
    ;; return nodes element and convert from vector to list.
    (-let* ((current-tree-node (treemacs-node-at-point))
            ((&TreeViewChildrenResult :nodes) (lsp-metals-treeview--send-treeview-children view-id node-uri))
            (children (append nodes nil)))
      (lsp-metals-treeview--log "Children returned:\n%s" (lsp--json-serialize nodes))
      (when (and (-non-nil children) current-tree-node)
        (lsp-metals-treeview--cache-add-nodes children current-tree-node))
      children)))

;;
;; UI tree view using treemacs
;;

(defun lsp-metals-treeview--xpm-pad (width)
  "Return an XPM image with the specified WIDTH."
  (format "/* XPM */
static char * pad_xpm[] = {
\"%d 1 1 1\",
\". c none\",
\"%s\"
};
" width (make-string width ?.)))

(defun lsp-metals-treeview--icon (metals-node open-form?)
  "Return icon based on METALS-NODE.
The icon will depend on the individual METALS-NODE and whether the
node is expanding based on OPEN-FORM? being True.  Check if icon matches
one of our icons for the Metals theme and if not display a standard +/-
if this is an expandable node.  If the node isn't expandable for now
do not show an icon."
  (-if-let* (((&TreeViewNode :icon?) metals-node)
             (icon (treemacs-get-icon-value icon? nil lsp-metals-treeview-theme)))
      icon
    (if (lsp-get metals-node :collapseState)
        (treemacs-get-icon-value
         (if open-form? 'expanded 'collapsed)
         nil
         lsp-treemacs-theme)

      ;; leaf node without an icon
      (concat (propertize " " 'display (create-image
                                        (lsp-metals-treeview--xpm-pad treemacs--icon-size)
                                        'xpm t
                                        :mask 'heuristic))
              " "))))

(defun lsp-metals-treeview--send-execute-command-async (command &optional args)
  "Create and send a `workspace/executeCommand'.
The message will contain the COMMAND and optional ARGS.  Send the
command asynchronously rather than the default `lsp-mode' of synchronous."
  (lsp-request-async "workspace/executeCommand"
                     (if args
                         (list :command command :arguments args)
                       (list :command command))
                     #'ignore
                     :mode 'detached))

(defun lsp-metals-treeview--exec-node-action (&rest _)
  "Execute the action associated with the treeview node."
  (-when-let* ((node (treemacs-button-get (treemacs-current-button) :node))
               ((&TreeViewNode :command?) node)
               ((&TreeViewCommand :command) command?))
    (with-lsp-workspace lsp-metals-treeview--current-workspace
      (pcase command
        ;; TODO: use `seq-first' after switching to emacs 27.
        (`"metals-echo-command" (lsp-metals-treeview--send-execute-command-async (seq-elt (lsp-get command? :arguments) 0)))
        (c (lsp-metals-treeview--send-execute-command-async c (lsp-get command? :arguments)))))))

(lsp-defun lsp-metals-treeview--on-node-collapsed ((&TreeViewNode :node-uri?) collapsed?)
  "Send metals/treeViewNodeCollapseDidChange to indicate collapsed/expanded.
Metals node is a node attached to treemacs in the :node key - passed as item
during render.  COLLAPSED? either t or nil dependong on if the node has been
collapsed or expanded."
  (lsp-metals-treeview--send-node-collapse-did-change lsp-metals-treeview--current-workspace
                                                      lsp-metals-treeview--view-id
                                                      node-uri?
                                                      collapsed?))

;;
;; Icon theme for Metals treeview
;; Icons are partially taken from VSCode and partially from Metals:
;;   https://github.com/scalameta/metals-vscode/tree/master/icons
;;   https://github.com/microsoft/vscode-codicons/tree/main/src/icons
;;
(treemacs-create-theme "Metals"
  :icon-directory (f-join lsp-metals-treeview--dir lsp-metals-treeview--icon-dir)
  :config
  (progn
    (treemacs-create-icon :file "logo.png" :extensions (root))
    (treemacs-create-icon :file "method.png" :extensions ("method"))
    (treemacs-create-icon :file "class.png" :extensions ("class"))
    (treemacs-create-icon :file "object.png" :extensions ("object"))
    (treemacs-create-icon :file "enum.png" :extensions ("enum"))
    (treemacs-create-icon :file "field.png" :extensions ("field"))
    (treemacs-create-icon :file "interface.png" :extensions ("interface"))
    (treemacs-create-icon :file "trait.png" :extensions ("trait"))
    (treemacs-create-icon :file "val.png" :extensions ("val"))
    (treemacs-create-icon :file "var.png" :extensions ("var"))
    (treemacs-create-icon :file "doctor.png" :extensions ("doctor"))))

(treemacs-create-theme "Metals-dark"
  :icon-directory (f-join lsp-metals-treeview--dir lsp-metals-treeview--icon-dir)
  :extends "Metals"
  :config
  (progn
    (treemacs-create-icon :file "book-dark.png" :extensions ("book"))
    (treemacs-create-icon :file "bug-dark.png" :extensions ("bug"))
    (treemacs-create-icon :file "github-dark.png" :extensions ("github"))
    (treemacs-create-icon :file "gitter-dark.png" :extensions ("gitter"))
    (treemacs-create-icon :file "issue-opened-dark.png" :extensions ("issue-opened"))
    (treemacs-create-icon :file "twitter-dark.png" :extensions ("twitter"))
    (treemacs-create-icon :file "discord-dark.png" :extensions ("discord"))
    (treemacs-create-icon :file "cancel-dark.png" :extensions ("cancel"))
    (treemacs-create-icon :file "cascade-dark.png" :extensions ("cascade"))
    (treemacs-create-icon :file "clean-all-dark.png" :extensions ("clean-all"))
    (treemacs-create-icon :file "clean-dark.png" :extensions ("clean"))
    (treemacs-create-icon :file "compile-dark.png" :extensions ("compile"))
    (treemacs-create-icon :file "connect-dark.png" :extensions ("connect"))
    (treemacs-create-icon :file "exception-dark.png" :extensions ("exception"))
    (treemacs-create-icon :file "focus-dark.png" :extensions ("focus"))
    (treemacs-create-icon :file "info-dark.png" :extensions ("info"))
    (treemacs-create-icon :file "location-dark.png" :extensions ("location"))
    (treemacs-create-icon :file "empty-window-dark.png" :extensions ("empty-window"))
    (treemacs-create-icon :file "sync-dark.png" :extensions ("sync"))
    (treemacs-create-icon :file "debug-stop-dark.png" :extensions ("debug-stop"))
    (treemacs-create-icon :file "close-dark.png" :extensions ("notifications-clear"))))

(treemacs-create-theme "Metals-light"
  :icon-directory (f-join lsp-metals-treeview--dir lsp-metals-treeview--icon-dir)
  :extends "Metals"
  :config
  (progn
    (treemacs-create-icon :file "book-light.png" :extensions ("book"))
    (treemacs-create-icon :file "bug-light.png" :extensions ("bug"))
    (treemacs-create-icon :file "github-light.png" :extensions ("github"))
    (treemacs-create-icon :file "gitter-light.png" :extensions ("gitter"))
    (treemacs-create-icon :file "issue-opened-light.png" :extensions ("issue-opened"))
    (treemacs-create-icon :file "twitter-light.png" :extensions ("twitter"))
    (treemacs-create-icon :file "discord-light.png" :extensions ("discord"))
    (treemacs-create-icon :file "cancel-light.png" :extensions ("cancel"))
    (treemacs-create-icon :file "cascade-light.png" :extensions ("cascade"))
    (treemacs-create-icon :file "clean-all-light.png" :extensions ("clean-all"))
    (treemacs-create-icon :file "clean-light.png" :extensions ("clean"))
    (treemacs-create-icon :file "compile-light.png" :extensions ("compile"))
    (treemacs-create-icon :file "connect-light.png" :extensions ("connect"))
    (treemacs-create-icon :file "exception-light.png" :extensions ("exception"))
    (treemacs-create-icon :file "focus-light.png" :extensions ("focus"))
    (treemacs-create-icon :file "info-light.png" :extensions ("info"))
    (treemacs-create-icon :file "location-light.png" :extensions ("location"))
    (treemacs-create-icon :file "empty-window-light.png" :extensions ("empty-window"))
    (treemacs-create-icon :file "sync-light.png" :extensions ("sync"))
    (treemacs-create-icon :file "debug-stop-light.png" :extensions ("debug-stop"))
    (treemacs-create-icon :file "close-light.png" :extensions ("notifications-clear"))))

(treemacs-define-expandable-node-type metals-node
  :open-icon (lsp-metals-treeview--icon item t)
  :closed-icon (lsp-metals-treeview--icon item nil)
  :label (propertize (lsp-get item :label) 'face 'default 'help-echo (lsp-get item :tooltip))
  :key (lsp-get item :nodeUri)
  :ret-action 'lsp-metals-treeview--exec-node-action
  :children (-when-let* ((node (treemacs-button-get btn :node))
                         ((&TreeViewNode :view-id :node-uri?) node))
              (lsp-metals-treeview--get-children view-id node-uri?))
  :child-type 'metals-node
  :more-properties `(:node
                     ,item
                     :leaf
                     ,(not (lsp-get item :collapseState)))
  :on-expand (lsp-metals-treeview--on-node-collapsed
              (treemacs-button-get btn :node) nil)
  :on-collapse (lsp-metals-treeview--on-node-collapsed
                (treemacs-button-get btn :node) t))

(treemacs-define-variadic-entry-node-type metals-root
  :key 'metals-root
  :children (lsp-metals-treeview--get-children lsp-metals-treeview--view-id)
  :child-type 'metals-node)

(defun lsp-metals-treeview (&optional workspace)
  "Display the Metals treeview window for the WORKSPACE (optional).
If WORKSPACE is not specified obtain the current workspace for the file in
the current buffer."
  (interactive)
  (-if-let* ((workspace
              (or workspace
                  (lsp-find-workspace lsp-metals-treeview--metals-server-id
                                      (buffer-file-name)))))
      (lsp-metals-treeview--show-window workspace t)
    (message "Current buffer is not within Metals workspace")))

(defun lsp-metals-treeview-reveal ()
  "Find the current buffer file in the treeview."
  (interactive)
  (-let (((&TreeViewRevealResult :view-id :uri-chain) (lsp-request "metals/treeViewReveal" (lsp--text-document-position-params))))
    (-when-let* ((workspace (lsp-find-workspace lsp-metals-treeview--metals-server-id (buffer-file-name)))
                 (treeview-buffer-name (lsp-metals-treeview--buffer-name workspace view-id)))
      (with-current-buffer treeview-buffer-name
        (mapc (lambda (uri)
                (-when-let (tree-node (lsp-metals-treeview--find-node uri))
                  (goto-char (marker-position tree-node))
                  (treemacs-update-node (treemacs-button-get tree-node :path) t)))
              (reverse uri-chain))
        (-when-let (buffer-window (get-buffer-window))
          (set-window-point buffer-window (point))
          (select-window buffer-window))))))


;; Debug helpers to track down issues with treemacs and aid development.
(defun lsp-metals-treeview--debug-node ()
  "Debug helper function to display treemacs node information."
  (interactive)
  (-let [node (treemacs-node-at-point)]
    (message
     "Label: %s
Depth: %s
Key: %s
Path: %s
State: %s
Parent: %s
eldoc: %s
Metals Item: %s"
     (treemacs--get-label-of node)
     (treemacs-button-get node :depth)
     (treemacs-button-get node :key)
     (treemacs-button-get node :path)
     (treemacs-button-get node :state)
     (-some-> node (treemacs-button-get :parent) (treemacs--get-label-of))
     (treemacs-button-get node :eldoc)
     (-some-> node (treemacs-button-get :node)))))


(provide 'lsp-metals-treeview)
;;; lsp-metals-treeview.el ends here

;; Local Variables:
;; End:
