hackerbase/src/listing.scm

238 lines
6.8 KiB
Scheme

;;
;; listing.scm
;;
;; Source listing with line highlights.
;;
;; ISC License
;;
;; Copyright 2023 Brmlab, z.s.
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
;;
;; Permission to use, copy, modify, and/or distribute this software
;; for any purpose with or without fee is hereby granted, provided
;; that the above copyright notice and this permission notice appear
;; in all copies.
;;
;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
;;
(declare (unit listing))
(module
listing
(
print-source-listing
listing-tests!
)
(import scheme
(chicken base)
(chicken string)
(chicken format)
testing
ansi
racket-kwargs
util-bst-ldict
table)
;; Returns the number of digits required to represent a given number
;; in decimal format.
(define (number-digits number)
(let loop ((number (abs number))
(digits 0))
(if (= number 0)
(if (= digits 0)
1
digits)
(loop (quotient number 10)
(+ digits 1)))))
;; Formats line number padding it with spaces from the left for
;; alignment with number of given maximum number of digits and
;; appends ordinal dot and space.
(define (format-line-number number digits)
(let loop ((args (list (sprintf "~A. " number)))
(spaces (- digits (number-digits number))))
(if (<= spaces 0)
(string-intersperse args "")
(loop (cons " " args)
(- spaces 1)))))
;; Returns true if given line is near the target line.
(define (line-near-target? line target context)
(let ((target-line (if (list? target)
(car target)
target)))
(or (< context 0)
(<= (abs (- line target-line)) context))))
;; Returns true if given line is near one of the target lines given.
(define (line-near-targets? line targets context)
(let loop ((targets targets))
(if (null? targets)
(< context 0)
(if (line-near-target? line (car targets) context)
#t
(loop (cdr targets))))))
;; Returns true if given number is in highlights.
(define (in-highlights? number highlights)
(let loop ((highlights highlights))
(if (null? highlights)
#f
(let* ((highlight (car highlights))
(line-number (if (list? highlight)
(car highlight)
highlight)))
(if (= number line-number)
#t
(loop (cdr highlights)))))))
;; Returns hihglight match - each highlight should contain:
;; line-number, message, stage, type (error, warning, info ...)
(define (match-highlight number highlights)
(let loop ((highlights highlights))
(if (null? highlights)
#f
(let* ((highlight (car highlights))
(line-number (if (list? highlight)
(car highlight)
highlight)))
(if (= number line-number)
highlight
(loop (cdr highlights)))))))
;; Returns comment if there is any
(define (highlight-comment number highlights)
(let loop ((highlights highlights))
(if (null? highlights)
#f
(let* ((highlight (car highlights))
(line-number (if (list? highlight)
(car highlight)
highlight)))
(if (= number line-number)
(if (list? highlight)
(cadr highlight)
#f)
(loop (cdr highlights)))))))
;; Prints and highlights a selection of source listing lines and
;; their optional context.
(define* (print-source-listing
lines
highlights
#:highlight-rules (highlight-rules
`((error ,(ansi #:bold #:red) ,(ansi #:default))
(warning ,(ansi #:yellow) ,(ansi #:default))
(info ,(ansi #:cyan) ,(ansi #:default))
))
#:ellipsis (ellipsis "...")
#:context-pre (ctx-pre "")
#:context-post (ctx-post "")
#:highlight-pre (hl-pre a:error)
#:highlight-post (hl-post a:default)
#:context (context 3)
#:keys (keys '(number -- line -- comment)))
(let loop ((lines lines)
(number 1)
(printed-something #f)
(was-printing #f)
(rtbl '()))
(if (null? lines)
(print (table->string (reverse rtbl)
))
(let* ((highlight (match-highlight number highlights))
(hl-type (if highlight (cadddr highlight) #f))
(hl-def (assq hl-type highlight-rules))
(hl-pre-real (if hl-def (cadr hl-def) hl-pre))
(hl-post-real (if hl-def (caddr hl-def) hl-post))
(context? (and (not highlight)
(line-near-targets? number highlights context)))
(print? (or highlight context?)))
(if print?
(let ((line (car lines))
(rtbl (if (and printed-something
(not was-printing))
(cons (list "" ellipsis)
rtbl)
rtbl))
(pre-str (if highlight
hl-pre-real
(if context?
ctx-pre
"")))
(post-str (if highlight
hl-post-real
(if context?
ctx-post
"")))
(comment (highlight-comment number highlights))
)
(loop (cdr lines)
(add1 number)
#t
#t
(cons (map (lambda (key)
(case key
((number)
(format "\t~A~A. ~A" pre-str number post-str))
((line)
(format "~A~A~A"
pre-str
(if (ldict? line)
(ldict-ref line 'line)
line)
post-str))
((comment)
(if comment
(format "~A # <<< ~A~A"
pre-str
comment
post-str)
""))
((--)
"\xc2\xa0")
(else
(format "~A~A ~A"
pre-str
(if (ldict? line)
(ldict-ref line key "--")
"**")
post-str))))
keys)
rtbl)))
(loop (cdr lines)
(+ number 1)
printed-something
#f
rtbl))))))
;; Performs self-tests of the listing module.
(define (listing-tests!)
(run-tests
listing
(test-eq? number-digits (number-digits 0) 1)
(test-eq? number-digits (number-digits 1) 1)
(test-eq? number-digits (number-digits 9) 1)
(test-eq? number-digits (number-digits 10) 2)
(test-eq? number-digits (number-digits 999) 3)
(test-equal? format-line-number (format-line-number 5 3) " 5. ")
(test-true line-near-target? (line-near-target? 4 5 -1))
(test-true line-near-target? (line-near-target? 4 5 1))
(test-true line-near-target? (line-near-target? 1 5 10))
(test-false line-near-target? (line-near-target? 4 5 0))
(test-true line-near-targets? (line-near-targets? 4 '(1 5 10) 1))
(test-false line-near-targets? (line-near-targets? 3 '(1 5 10) 1))
(test-true line-near-targets? (line-near-targets? 3 '(1 5 10) 2))
))
)