expr: 26.97%

#lang racket/base
(provide create-record-maintainer
         get-record-maintainer
         terminate-record-maintainer)
(require racket/path
         racket/future
         racket/function
         racket/match
         data/interval-map
         sauron/collect/record
         sauron/collect/collector
         sauron/log)

(define (valid-path? file-path)
  (and (file-exists? file-path)
       (path-has-extension? file-path #".rkt")))

(define path=>maintainer (make-hash))
;;; The constraint is only `record-maintainer-creator` allowed to create new maintainer
; but anyone might like to create one concurrently, so `hash-ref!` here blocked all repeated creation
; since `thread-receive` ensure every creation is proceeded one by one
; if the previous `loop` created one, the next `loop` will skip existing creation
(define record-maintainer-creator
  (thread
   (thunk
    (let loop ()
      (match (thread-receive)
        [(list 'create from path)
         (define mt (hash-ref! path=>maintainer path (thunk (make-record-maintainer path))))
         ; `from` must be another thread
         (when from
           (thread-send from mt))])
      (loop)))))

(define (create-record-maintainer path [from #f])
  ; only create maintainer for valid path
  (when (valid-path? path)
    (future (thunk (hash-set! path=>maintainer path (thunk (make-record-maintainer path)))))))

(define (get-record-maintainer path #:wait? [wait? #f])
  (cond
    [(not (valid-path? path))
     ; when path is invalid, return same thread to handle all non-sense requirement
     ; since only one-more thread here, it should not be a big overhead
     (log:warning "cannot create maintainer for invalid path: ~a" path)
     do-nothing]
    [wait? (thread-send record-maintainer-creator
                        (list 'create (current-thread) path))
           (thread-receive)]
    [else (hash-ref path=>maintainer path #f)]))

(define (terminate-record-maintainer path)
  (when (valid-path? path)
    (define maintainer (get-record-maintainer path))
    (when maintainer
      (hash-set! path=>maintainer path #f)
      (kill-thread maintainer))))

;;; this thread do nothing and provide fake reply is need
; the purpose is making sure the caller will fail gratefully, but no need to handle exception
; this is because the caller already think cannot fetch data is normal
; in editor, users can always try to get jump to definition even no definition exists
; so caller will just ignore the operation, thus, another error handling shouldn't be there
(define do-nothing (thread (thunk (let loop ()
                                    (match (thread-receive)
                                      [(list 'get-record from)
                                       (thread-send from (make-record))]
                                      [else (void)])
                                    (loop)))))

(define (make-record-maintainer file-path)
  (thread
   (thunk
    (define cached-record (collect-from file-path))
    (let loop ()
      (match (thread-receive)
        [(list 'update)
         (match-define (struct* record ([created-time created-time])) cached-record)
         (when (< created-time (file-or-directory-modify-seconds file-path))
           (set! cached-record (collect-from file-path)))]

        [(list 'require-location? from require)
         (define requires (record-requires cached-record))
         (thread-send from (hash-ref requires require #f))]

        [(list 'get-doc from pos)
         (define doc (record-doc cached-record))
         (thread-send from (interval-map-ref doc pos #f))]

        [(list 'jump-to-def from from-pos)
         (define bindings (record-bindings cached-record))
         (thread-send from (interval-map-ref bindings from-pos #f))]

        [(list 'get-def from id)
         (define defs (record-defs cached-record))
         (thread-send from (hash-ref defs id #f))]
        )
      (loop)))))