expr: 14.55%

#lang racket/gui
#|
NOTICE: modify from example in https://github.com/racket/gui/blob/master/gui-doc/mrlib/scribblings/hierlist/hierlist.scrbl based on MIT/APACHE2.0
origin author: https://github.com/racket/gui/graphs/contributors
modifier author: Lîm Tsú-thuàn(GitHub: @dannypsnl)
|#
(provide project-files-pane%)
(require mrlib/hierlist
         file-watchers
         framework/preferences
         sauron/path/ignore
         sauron/path/util
         sauron/collect/api
         sauron/collect/record-maintainer
         sauron/project/dir-state
         sauron/path/renamer)

(let ([cache-project-dir #f]
      [cache-project-watcher #f])
  (preferences:add-callback
   'current-project
    (_ new-proj-dir)
     (when (path-string? new-proj-dir)
       (unless (equal? new-proj-dir cache-project-dir)
         ; stop project watcher if existed
         (when cache-project-watcher
           (kill-thread cache-project-watcher))
         ; reset the project watcher
         (set! cache-project-watcher (robust-watch new-proj-dir))
         ; start creating
         (start-tracking new-proj-dir ignore?)
         ; reset the project directory cache
         (set! cache-project-dir new-proj-dir)))))
  (void))

(define set-text-mixin
  (mixin (hierarchical-list-item<%>) ((interface () set-text get-text))
    (inherit get-editor)
    (super-new)

    ; get-text: return the label of item
    (define/public (get-text)
      (define t (get-editor)) ; a text% object
      (send t get-text))

    ; set-text: this sets the label of the item
    (define/public (set-text str)
      (define t (get-editor)) ; a text% object
      (send t erase)
      (send t insert str))))

(struct selected (dir file parent-dir) #:transparent)

(define project-files%
  (class hierarchical-list%
    (init editor-panel)
    (define the-editor-panel editor-panel)
    (define table-path=>item (make-hash))
    (define (path->key path) (build-path path "$$"))
    (define (get-item-by-path path)
      (hash-ref table-path=>item (path->key path)))
    (define (store-item-into-path path item)
      (hash-set! table-path=>item (path->key path) item))
    (define (remove-item path)
      (define item (get-item-by-path path))
      (define parent-item (send item get-parent))
      (send parent-item delete-item item)
      (hash-remove! table-path=>item (path->key path)))

    ; new-item : create new item for a file or directory
    (define (new-item parent-dir directory subpath)
      (when (dir-open? directory)
        (send parent-dir open))
      (define cur-path (build-path directory subpath))
      (when (not (ignore? subpath))
        (match (file-or-directory-type cur-path #t)
          ['file
           (define item (send parent-dir new-item set-text-mixin))
           (send* item
             [set-text (path->string subpath)]
             [user-data (selected directory cur-path directory)])
           (store-item-into-path cur-path item)]
          ['directory
           (define item (send parent-dir new-list set-text-mixin))
           (send* item
             [set-text (path->string subpath)]
             [user-data (selected cur-path cur-path directory)])
           (store-item-into-path cur-path item)
           (for ([subpath (directory-list cur-path)])
             (new-item item cur-path subpath))]
          ['link (void)])))

    (define/public (get-cur-selected-dir) (selected-dir current-selected))
    (define/public (get-cur-selected-file) (selected-file current-selected))
    (define/public (get-cur-selected-parent-dir) (selected-parent-dir current-selected))

    (define/override (on-select i) (set! current-selected (send i user-data)))
    (define/override (on-double-select i)
      (define path (selected-file (send i user-data)))
      (when (file-exists? path) ;; when double-click a file, open it in editor
        (define tab (send the-editor-panel get-current-tab))
        (let ([tab-<?> (send the-editor-panel find-matching-tab path)])
          (if tab-<?>
              (send the-editor-panel change-to-tab tab-<?>)
              (send the-editor-panel open-in-new-tab path)))
        (unless (send (send tab get-defs) get-filename)
          (send the-editor-panel close-given-tab tab))))

    (define/override (on-item-opened i)
      (match-define (struct* selected ([dir dir])) (send i user-data))
      (open-dir dir))
    (define/override (on-item-closed i)
      (match-define (struct* selected ([dir dir])) (send i user-data))
      (close-dir dir))

    ;;; init
    (super-new)
    (define top-dir-list (send this new-list set-text-mixin))
    (define current-selected #f)
    ;;; listener
    (thread  ()
              (let loop ()
                (match (file-watcher-channel-get)
                  [(list 'robust 'add path)
                   (when (not (ignore? path))
                     (create path)
                     ;;; insert item
                     (new-item (get-item-by-path (parent-path path))
                               (path-only path)
                               (basepath path))
                     (send this sort  (lhs rhs)
                                       (string<? (send lhs get-text)
                                                 (send rhs get-text)))))]
                  [(list 'robust 'remove path)
                   (when (not (ignore? path))
                     (terminate-record-maintainer path)
                     (remove-item path))]
                  [(list 'robust 'change path)
                   (when (not (ignore? path))
                     (update path))]
                  [else (void)])
                (loop))))

    ; Build a fresh file-tree-view for certain directory
    (define (fresh-file-tree dir)
      (set! current-selected (selected dir #f #f))
      (send this delete-item top-dir-list)
      (set! top-dir-list (send this new-list set-text-mixin))
      (send top-dir-list set-text (basename dir))
      ; add new-item for each member of dir
      (send top-dir-list user-data (selected dir dir #f))
      (store-item-into-path dir top-dir-list)
      (for ([sub (directory-list dir)])
        (new-item top-dir-list dir sub))
      ;; open top dir-list by default
      (send top-dir-list open))
    (preferences:add-callback 'current-project
                               (_ new-dir)
                                (when (path-string? new-dir)
                                  (fresh-file-tree new-dir))))))

(define project-files-pane%
  (class horizontal-pane%
    (init-field parent editor-panel)
    (super-new [parent parent])

    (define view (new project-files% [parent parent] [editor-panel editor-panel]))

    (define (add-file/dir btn event)
      (define new-frame (new frame% [label "New"] [width 300] [height 300]))
      (send* new-frame [show #t] [center])
      (define (ask box event)
        (send new-frame show #f)
        (define selected-dir (send view get-cur-selected-dir))
        (match (first (send box get-selections))
          [0
           (define file-name (get-text-from-user "name of file?" ""))
           (when file-name
             (define path (build-path selected-dir file-name))
             (ensure-file path))]
          [1
           (define dir-name (get-text-from-user "name of directory?" ""))
           (when dir-name
             (make-directory* (build-path selected-dir dir-name)))]))
      (new list-box% [parent new-frame] [label "New"] [choices '("file" "directory")] [callback ask]))
    (define (remove-path-and-refresh btn event)
      (delete-directory/files (send view get-cur-selected-file) #:must-exist? #f))
    (define (rename-path btn event)
      (define selected-dir (send view get-cur-selected-dir))
      (define selected-file (send view get-cur-selected-file))
      (define name (get-text-from-user "new name for selected path?" "" #f (basename selected-file)))
      (when name
        (define selected-parent-dir (send view get-cur-selected-parent-dir))
        (define old-path (or selected-file selected-dir))
        (define new-path (build-path selected-parent-dir name))
        (when (dir-open? old-path)
          (close-dir old-path)
          (open-dir new-path))
        (auto-rename (preferences:get 'current-project) editor-panel old-path new-path)))

    (new button% [parent this] [label "add"] [callback add-file/dir])
    (new button% [parent this] [label "remove"] [callback remove-path-and-refresh])
    (new button% [parent this] [label "rename"] [callback rename-path])))

(define (ensure-file path)
  (make-parent-directory* path)
  (close-output-port (open-output-file path #:exists 'append)))

(module+ main
  (define frame (new frame% [label "test: project files"] [width 300] [height 300]))

  (new project-files-pane% [parent frame] [editor-panel #f])
  (preferences:set-default 'current-project (current-directory) path-string?)

  (send frame center)
  (send frame show #t))

(module+ test
  (require rackunit)

  (test-case ""
             (ensure-file "tmp")
             (check-equal? (file-exists? "tmp") #t)
             (delete-directory/files "tmp")))