aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ccwl/ui.scm172
-rwxr-xr-xscripts/ccwl17
2 files changed, 185 insertions, 4 deletions
diff --git a/ccwl/ui.scm b/ccwl/ui.scm
new file mode 100644
index 0000000..97e5d09
--- /dev/null
+++ b/ccwl/ui.scm
@@ -0,0 +1,172 @@
+;;; ccwl --- Concise Common Workflow Language
+;;; Copyright © 2022 Arun Isaac <arunisaac@systemreboot.net>
+;;;
+;;; This file is part of ccwl.
+;;;
+;;; ccwl 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.
+;;;
+;;; ccwl 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 ccwl. If not, see <https://www.gnu.org/licenses/>.
+
+(define-module (ccwl ui)
+ #:use-module (rnrs io ports)
+ #:use-module (srfi srfi-26)
+ #:use-module (srfi srfi-28)
+ #:use-module (ccwl conditions)
+ #:export (report-ccwl-violation))
+
+(define (repeat thunk n)
+ "Call THUNK N times."
+ (unless (zero? n)
+ (thunk)
+ (repeat thunk (1- n))))
+
+(define (save-excursion thunk port)
+ "Call THUNK and restore PORT position to what it was before THUNK
+was executed. For the curious, this function is named after the
+save-excursion function in elisp."
+ (let ((original-position #f))
+ (dynamic-wind (lambda ()
+ (set! original-position (port-position port)))
+ thunk
+ (lambda ()
+ (set-port-position! port original-position)))))
+
+(define (read-end port)
+ "Return the position in PORT corresponding to the end of the
+S-expression starting at the current port position. This function does
+not affect the current port position."
+ (save-excursion (lambda ()
+ (read port)
+ (port-position port))
+ port))
+
+(define (read-sexp-string port)
+ "Read an S-expression from PORT and return it as a string with
+whitespace intact."
+ (get-string-n port (- (read-end port)
+ (port-position port))))
+
+(define (color code str)
+ "Wrap STR in ANSI escape CODE, thus rendering it in color in a
+terminal."
+ (format "~a[~am~a~a[0m" #\esc code str #\esc))
+
+(define bold (cut color 1 <>))
+(define red (cut color 31 <>))
+(define magenta (cut color 35 <>))
+
+(define (count-lines str)
+ "Count the number of lines in STR."
+ (call-with-input-string str
+ (lambda (port)
+ (let loop ()
+ (if (eof-object? (get-line port))
+ 0
+ (1+ (loop)))))))
+
+(define (number-of-digits n)
+ "Return the number of decimal digits in positive integer N."
+ (if (< n 10)
+ 1
+ (1+ (number-of-digits (quotient n 10)))))
+
+(define (display-integer n minimum-width port)
+ "Display integer N to PORT using at least MINIMUM-WIDTH
+characters. If N is not large enough, it is padded with spaces."
+ (display (make-string (max 0 (- minimum-width
+ (number-of-digits n)))
+ #\space)
+ port)
+ (display n port))
+
+(define (display-with-line-numbers str out starting-line-number)
+ "Display STR to port OUT with each line prefixed with a line
+number. Line numbers start from STARTING-LINE-NUMBER."
+ (call-with-input-string str
+ (lambda (in)
+ (let ((last-line-number (+ starting-line-number
+ (count-lines str))))
+ (let loop ((line-number starting-line-number))
+ (let ((line (get-line in)))
+ (unless (eof-object? line)
+ (display (make-string 4 #\space) out)
+ (display-integer line-number
+ (number-of-digits last-line-number)
+ out)
+ (display (format " | ~a~%" line) out)
+ (loop (1+ line-number)))))))))
+
+(define (put-line line port)
+ "Display LINE to PORT followed by a newline character."
+ (display line port)
+ (newline port))
+
+(define (string-blank? str)
+ "Return non-#f if STR contains only whitespace characters, else
+return #t."
+ (string-every char-set:whitespace str))
+
+(define (source-in-context file line-number column-number)
+ "Return source from FILE at LINE-NUMBER, COLUMN-NUMBER in context
+with S-expression at LINE-NUMBER, COLUMN-NUMBER highlit in
+red. LINE-NUMBER and COLUMN-NUMBER are zero-based."
+ (call-with-output-string
+ (lambda (out)
+ (call-with-input-file file
+ (lambda (in)
+ ;; Get to line preceding syntax x.
+ (repeat (cut get-line in)
+ (max 0 (1- line-number)))
+ ;; Display line preceding syntax x unless blank.
+ (let ((line (get-line in)))
+ (unless (or (zero? line-number)
+ (string-blank? line))
+ (put-line line out)))
+ ;; Display part of line before syntax x.
+ (display (get-string-n in column-number)
+ out)
+ ;; Display syntax x in red. Color each line separately to
+ ;; help line oriented functions like
+ ;; `display-with-line-numbers'.
+ (display (string-join (map (compose bold red)
+ (string-split (read-sexp-string in)
+ #\newline))
+ "\n")
+ out)
+ ;; (display (bold (red (read-sexp-string in)))
+ ;; out)
+ ;; Display part of line after syntax x.
+ (put-line (get-line in) out)
+ ;; Display line following syntax x unless blank.
+ (let ((line (get-line in)))
+ (unless (or (eof-object? line)
+ (string-blank? line))
+ (put-line line out))))))))
+
+(define (report-ccwl-violation exception)
+ (let ((file (ccwl-violation-file exception))
+ (line (ccwl-violation-line exception))
+ (column (ccwl-violation-column exception)))
+ (display (bold (format "~a:~a:~a: " file (1+ line) column))
+ (current-error-port))
+ (display (bold (red "error:"))
+ (current-error-port))
+ (display " " (current-error-port))
+ (display (apply format
+ (formatted-message-format exception)
+ (map (compose bold magenta)
+ (formatted-message-arguments exception)))
+ (current-error-port))
+ (newline (current-error-port))
+ (display-with-line-numbers (source-in-context file line column)
+ (current-error-port)
+ (max 1 line))))
diff --git a/scripts/ccwl b/scripts/ccwl
index 63a929a..1215486 100755
--- a/scripts/ccwl
+++ b/scripts/ccwl
@@ -24,12 +24,15 @@
;;; Code:
-(use-modules (srfi srfi-28)
+(use-modules (rnrs exceptions)
+ (srfi srfi-28)
(srfi srfi-37)
(ice-9 match)
(ccwl ccwl)
+ (ccwl conditions)
(ccwl cwl)
- (ccwl graphviz))
+ (ccwl graphviz)
+ (ccwl ui))
(define (invalid-option opt name arg result)
(error "Invalid option" name))
@@ -84,8 +87,14 @@ Compile SOURCE-FILE.
((cond
((string=? to "cwl") workflow->cwl)
((string=? to "dot") workflow->dot))
- (load (canonicalize-path (assq-ref args 'source-file))
- read-syntax)
+ (guard (exception
+ ;; Handle syntax violation exceptions by reporting
+ ;; them and exiting.
+ ((ccwl-violation? exception)
+ (report-ccwl-violation exception)
+ (exit #f)))
+ (load (canonicalize-path (assq-ref args 'source-file))
+ read-syntax))
(current-output-port)))))
((program args ...)
(let ((args (args-fold args