expr: 27.49%

#lang racket/gui

(provide collect-from)

(require drracket/check-syntax
         syntax/modread
         net/url
         data/interval-map
         try-catch-finally
         sauron/collect/binding
         sauron/collect/record
         sauron/log)

(define collector%
  (class (annotations-mixin object%)
    (init-field src text)

    (define doc (make-interval-map))
    (define bindings (make-interval-map))
    (define defs (make-hash))
    (define requires (make-hash))

    (define/override (syncheck:find-source-object stx) (and (equal? src (syntax-source stx)) src))

    (define/override (syncheck:add-docs-menu source-obj
                                             start
                                             end
                                             id
                                             _label
                                             path
                                             definition-tag
                                             url-tag)
      (when url
        (when (= start end)
          (set! end (add1 end)))
        (define path-url (path->url path))
        (define link+tag
          (cond
            [url-tag (struct-copy url path-url [fragment url-tag])]
            [definition-tag (struct-copy url path-url
                                         [fragment (def-tag->html-anchor-tag definition-tag)])]
            [else path-url]))
        (interval-map-set! doc start end (url->string link+tag))))

    (define/override (syncheck:add-require-open-menu source-obj start end required-file)
      (log:debug "require ~a" required-file)
      (hash-set! requires required-file (list start end)))

    (define/override (syncheck:add-arrow/name-dup start-src-obj start-left start-right
                                                  end-src-obj end-left end-right
                                                  actual?
                                                  level
                                                  require-arrow?
                                                  name-dup?)
      (define id (string->symbol (send text get-text end-left end-right)))
      (unless require-arrow?
        (interval-map-set! bindings
                           end-left
                           (add1 end-right)
                           (binding id start-left start-right #f))))

    (define/override (syncheck:add-jump-to-definition source-obj start end id filename submods)
      (log:debug "syncheck:add-jump-to-definition ~a" filename)
      (interval-map-set! bindings start (add1 end) (binding id #f #f filename)))

    (define/override (syncheck:add-definition-target source-obj start end id mods)
      ; Record a definition which named `id` in this document, maps its name `id` to its meta data,
      ; 1. start position
      ; 2. end position
      ; 3. source file
      ; so an external user can find where to jump.
      ;
      ; e.g.
      ;
      ;   (define id <expr>)
      (log:debug "syncheck:add-definition-target ~a:~a" source-obj id)
      (hash-set! defs id (binding id start end src)))

    (define/public (build-record)
      (make-record #:created-time (current-seconds)
                   #:doc doc
                   #:bindings bindings
                   #:defs defs
                   #:requires requires))
    (super-new)))

(define (collect-from path)
  (define text (new text%))
  (send text load-file path)
  (define collector (new collector% [src path] [text text]))
  (define-values (src-dir file dir?) (split-path path))
  (log:info "collect-from path: ~a" path)
  (define in (open-input-string (send text get-text)))

  (try (define ns (make-base-namespace))
       (define-values (add-syntax done) (make-traversal ns src-dir))
       (parameterize ([current-annotations collector]
                      [current-namespace ns]
                      [current-load-relative-directory src-dir])
         (define stx (expand (with-module-reading-parameterization  () (read-syntax path in)))))
         (add-syntax stx)
         (done))
       (log:info "collect-from path done: ~a" path)
       (catch _ (log:error "collect-from path: ~a failed" path)))
  (send collector build-record))

#|
NOTICE: based on MIT/APACHE2.0
modify from https://github.com/jeapostrophe/racket-langserver/blob/master/docs-helpers.rkt
origin author: https://github.com/jeapostrophe/racket-langserver/graphs/contributors
modifier author: Lîm Tsú-thuàn(GitHub: @dannypsnl)
|#
;; Example: '(def ((quote #%kernel) hasheq)) => "(def._((quote._~23~25kernel)._hasheq))"
;; mostly a copy of a closed function `anchor-name` in `scribble-lib/scribble/html-render.rkt`
(define (def-tag->html-anchor-tag v)
  (define (encode-byte b)
    (string-append (if (< b 16) "~0" "~") (number->string b 16)))
  (define (encode-bytes str)
    (string->bytes/utf-8 (encode-byte (bytes-ref str 0))))
  (let* ([v (string->bytes/utf-8 (format "~a" v))]
         [v (regexp-replace* #rx#"[A-Z.]" v #".&")]
         [v (regexp-replace* #rx#" " v #"._")]
         [v (regexp-replace* #rx#"\"" v #".'")]
         [v (regexp-replace* #rx#"[^-a-zA-Z0-9_!+*'()/.,]" v encode-bytes)])
    (bytes->string/utf-8 v)))

(module+ main
  (record-doc (collect-from (normalize-path "collector.rkt"))))