Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configurable colors #11

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 13 additions & 6 deletions README.md
Expand Up @@ -6,6 +6,8 @@ For example, to print a nice stack trace in a REPL:


=> (use 'clj-stacktrace.repl) => (use 'clj-stacktrace.repl)
=> ("foo") => ("foo")
java.lang.ClassCastException: java.lang.String cannot be cast to clojure.lang.IFn (NO_SOURCE_FILE:0)
=> (pst)
java.lang.ClassCastException: java.lang.String cannot be cast to clojure.lang.IFn (NO_SOURCE_FILE:0) java.lang.ClassCastException: java.lang.String cannot be cast to clojure.lang.IFn (NO_SOURCE_FILE:0)
Compiler.java:5440 clojure.lang.Compiler.eval Compiler.java:5440 clojure.lang.Compiler.eval
Compiler.java:5391 clojure.lang.Compiler.eval Compiler.java:5391 clojure.lang.Compiler.eval
Expand All @@ -25,18 +27,23 @@ For example, to print a nice stack trace in a REPL:
NO_SOURCE_FILE:2 user/eval100 NO_SOURCE_FILE:2 user/eval100
Compiler.java:5424 clojure.lang.Compiler.eval Compiler.java:5424 clojure.lang.Compiler.eval



In stack traces printed by `pst`: In stack traces printed by `pst`:


* Java methods are described with the usual `name.space.ClassName.methodName` convention and Clojure functions with their own `name.space/function-name` convention. * Java methods are described with the usual
* Anonymous clojure functions are denoted by adding an `[fn]` to their enclosing, named function. `name.space.ClassName.methodName` convention and Clojure functions
with their own `name.space/function-name` convention.
* Anonymous clojure functions are denoted by adding an `[fn]` to their
enclosing, named function.
* "Caused by" cascades are shown as in regular java stack traces. * "Caused by" cascades are shown as in regular java stack traces.
* Elements are vertically aligned for better readability. * Elements are vertically aligned for better readability.
* Printing is directed to `*out*`. * Printing is directed to `*out*`.


If you want to direct the printing to somewhere other than `*out*`, either use `pst-on` to specify the output location or `pst-str` to capture the printing as a string. If you want to direct the printing to somewhere other than `*out*`,
either use `pst-on` to specify the output location.


The library also offers an API for programatically 'parsing' exceptions. This API is used internal for `pst` and can be used to e.g. improve development tools. Try for example: The library also offers an API for programatically 'parsing'
exceptions. This API is used internal for `pst` and can be used to
e.g. improve development tools. Try for example:


```clj ```clj
(use 'clj-stacktrace.core) (use 'clj-stacktrace.core)
Expand All @@ -46,7 +53,7 @@ The library also offers an API for programatically 'parsing' exceptions. This AP
(parse-exception e))) (parse-exception e)))
``` ```


If you use Leiningen, you can install clj-stacktrace on a per-user basis: If you use Leiningen, you can install clj-stacktrace on a user-level basis:


$ lein plugin install clj-stacktrace 0.2.4 $ lein plugin install clj-stacktrace 0.2.4


Expand Down
4 changes: 2 additions & 2 deletions src/clj_stacktrace/core.clj
Expand Up @@ -76,7 +76,7 @@
:method (.getMethodName elem))))) :method (.getMethodName elem)))))


(defn parse-trace-elems (defn parse-trace-elems
"Returns a seq of maps providing usefull information about the java stack "Returns a seq of maps providing useful information about the java stack
trace elements. See parse-trace-elem." trace elements. See parse-trace-elem."
[elems] [elems]
(map parse-trace-elem elems)) (map parse-trace-elem elems))
Expand Down Expand Up @@ -110,7 +110,7 @@
base))) base)))


(defn parse-exception (defn parse-exception
"Returns a Clojure map providing usefull informaiton about the exception. "Returns a Clojure map providing useful information about the exception.
The map has keys The map has keys
:class Class of the exception. :class Class of the exception.
:message Regular exception message string. :message Regular exception message string.
Expand Down
140 changes: 80 additions & 60 deletions src/clj_stacktrace/repl.clj
@@ -1,37 +1,51 @@
(ns clj-stacktrace.repl (ns clj-stacktrace.repl
(:use clj-stacktrace.core) (:use [clj-stacktrace.core :only [parse-exception]]
(:require [clj-stacktrace.utils :as utils])) [clj-stacktrace.utils :only [omit-frames fence rjust]]))


(def color-codes (def color-codes
{:red "\033[31m" {:red "\033[31m"
:green "\033[32m" :green "\033[32m"
:yellow "\033[33m" :yellow "\033[33m"
:blue "\033[34m" :blue "\033[34m"
:magenta "\033[35m" :magenta "\033[35m"
:cyan "\033[36m" :cyan "\033[36m"
:default "\033[39m"})

:red-bg "\033[41m"
(defn- colored :green-bg "\033[42m"
[color? color text] :yellow-bg "\033[43m"
(if color? :blue-bg "\033[44m"
(str (color-codes color) text (color-codes :default)) :magenta-bg "\033[45m"
text)) :cyan-bg "\033[46m"})


(defn elem-color (def ^{:private true} default-colors {:error-color :red
"Returns a symbol identifying the color appropriate for the given trace elem. :user-code-color :green
:green All Java elems :repl-color :yellow
:yellow Any fn in the user or repl* namespaces (i.e. entered at REPL) :java-color :blue
:blue Any fn in clojure.* (e.g. clojure.core, clojure.contrib.*) :clojure-color :magenta
:magenta Anything else - i.e. Clojure libraries and app code." :clojure-java-color :cyan})

(defn- colored [color? color text color-overrides]
(let [colors (merge default-colors color-overrides)]
(if color?
(str (color-codes (colors color)) text "\033[39m")
text)))

(defn- elem-color
"Returns a keyword identifying the color appropriate for the given trace elem.
:clojure-java-color Java elems in clojure.*
:java-color Any other Java elems
:repl-color Any fn in the user or repl* namespaces (i.e. entered at REPL)
:clojure-color Any fn in clojure.* (e.g. clojure.core, clojure.contrib.*)
:user-code-color Anything else - i.e. Clojure libraries and app code."
[elem] [elem]
(if (:java elem) (if (:java elem)
(if (re-find #"^clojure\." (:class elem)) (if (re-find #"^clojure\." (:class elem))
:cyan :clojure-java-color
:blue) :java-color)
(cond (nil? (:ns elem)) :yellow (cond (nil? (:ns elem)) :repl-color
(re-find #"^(user|repl)" (:ns elem)) :yellow (re-find #"^(user|repl)" (:ns elem)) :repl-color
(re-find #"^clojure\." (:ns elem)) :magenta (re-find #"^clojure\." (:ns elem)) :clojure-color
:user-code :green))) :else :user-code-color )))


(defn source-str [parsed] (defn source-str [parsed]
(if (and (:file parsed) (:line parsed)) (if (and (:file parsed) (:line parsed))
Expand All @@ -47,76 +61,82 @@
(defn method-str [parsed] (defn method-str [parsed]
(if (:java parsed) (java-method-str parsed) (clojure-method-str parsed))) (if (:java parsed) (java-method-str parsed) (clojure-method-str parsed)))


(defn pst-class-on [^java.io.Writer on color? ^Class class] (defn pst-class-on [^java.io.Writer on color? ^Class class color-overrides]
(.append on ^String (colored color? :red (str (.getName class) ": "))) (.append on ^String (colored color? :error-color (str (.getName class) ": ") color-overrides))
(.flush on)) (.flush on))


(defn pst-message-on [^java.io.Writer on color? message] (defn pst-message-on [^java.io.Writer on color? message color-overrides]
(.append on ^String (colored color? :red message)) (.append on ^String (colored color? :error-color message color-overrides))
(.append on "\n") (.append on "\n")
(.flush on)) (.flush on))


(defn pst-elem-str (defn pst-elem-str
[color? parsed-elem print-width] [color? parsed-elem print-width color-overrides]
(colored color? (elem-color parsed-elem) (colored color? (elem-color parsed-elem)
(str (utils/rjust print-width (source-str parsed-elem)) (str (rjust print-width (source-str parsed-elem))
" " (method-str parsed-elem)))) " " (method-str parsed-elem))
color-overrides))


(defn pst-elems-on (defn pst-elems-on
[^java.io.Writer on color? parsed-elems & [source-width]] [^java.io.Writer on color? parsed-elems & [source-width color-overrides]]
(let [print-width (+ 6 (or source-width (let [print-width (+ 6 (or source-width
(utils/fence (fence (sort (for [elem parsed-elems]
(sort (count (source-str elem)))))))]
(map #(.length ^String %)
(map source-str parsed-elems))))))]
(doseq [parsed-elem parsed-elems] (doseq [parsed-elem parsed-elems]
(.append on ^String (pst-elem-str color? parsed-elem print-width)) (.append on ^String (pst-elem-str color? parsed-elem print-width color-overrides))
(.append on "\n") (.append on "\n")
(.flush on)))) (.flush on))))


(defn pst-caused-by-on (defn pst-caused-by-on
[^java.io.Writer on color?] [^java.io.Writer on color? color-overrides]
(.append on ^String (colored color? :red "Caused by: ")) (.append on ^String (colored color? :error-color "Caused by: " color-overrides))
(.flush on)) (.flush on))


(defn- pst-cause-on (defn- pst-cause-on
[^java.io.Writer on color? exec source-width] [^java.io.Writer on exec {:keys [source-width omit color?]} color-overrides]
(pst-caused-by-on on color?) (pst-caused-by-on on color? color-overrides)
(pst-class-on on color? (:class exec)) (pst-class-on on color? (:class exec) color-overrides)
(pst-message-on on color? (:message exec)) (pst-message-on on color? (:message exec) color-overrides)
(pst-elems-on on color? (:trimmed-elems exec) source-width) (pst-elems-on on color? (omit-frames (:trimmed-elems exec) omit)
source-width color-overrides)
(if-let [cause (:cause exec)] (if-let [cause (:cause exec)]
(pst-cause-on on color? cause source-width))) (pst-cause-on on color? cause source-width color-overrides)))


(defn find-source-width (defn find-source-width
"Returns the width of the longest source-string among all trace elems of the "Returns the width of the longest source-string among all trace elems of the
excp and its causes." excp and its causes."
[excp] [excp]
(let [this-source-width (->> (:trace-elems excp) (let [this-source-width (->> (:trace-elems excp)
(map (comp count source-str)) (map (comp count source-str))
(sort) (sort) (fence))]
(utils/fence))]
(if-let [cause (:cause excp)] (if-let [cause (:cause excp)]
(max this-source-width (find-source-width cause)) (max this-source-width (find-source-width cause))
this-source-width))) this-source-width)))


(defn pst-on [on color? e] (defn pst-on
"Prints to the given Writer on a pretty stack trace for the given exception e, "Prints to the given Writer on a pretty stack trace for the given exception e,
ANSI colored if color? is true." ANSI colored if color? is true."
[on e {:keys [omit color?] :as opts}]
(let [exec (parse-exception e) (let [exec (parse-exception e)
source-width (find-source-width exec)] source-width (find-source-width exec)
(pst-class-on on color? (:class exec)) color? (or color? (:color? opts) (:test-color opts))
(pst-message-on on color? (:message exec)) color-overrides (select-keys opts (keys default-colors))]
(pst-elems-on on color? (:trace-elems exec) source-width) (pst-class-on on color? (:class exec) color-overrides)
(pst-message-on on color? (:message exec) color-overrides)
(pst-elems-on on color? (omit-frames (:trace-elems exec) omit) source-width color-overrides)
(if-let [cause (:cause exec)] (if-let [cause (:cause exec)]
(pst-cause-on on color? cause source-width)))) (pst-cause-on on cause
(assoc opts
:source-width source-width
:color? color?)
color-overrides))))


(defn pst (defn pst
"Print to *out* a pretty stack trace for an exception, by default *e." "Print to *out* a pretty stack trace for an exception, by default *e."
[& [e]] [& [e & {:as opts}]]
(pst-on *out* false (or e *e))) (pst-on *out* (or e *e) opts))


(defn pst+ (defn pst+
"Like pst, but with ANSI terminal color coding." "Like pst, but with ANSI terminal color coding."
[& [e]] [& [e & {:as opts}]]
(pst-on *out* true (or e *e))) (pst-on *out* (or e *e) (assoc opts :color? true)))
16 changes: 16 additions & 0 deletions src/clj_stacktrace/utils.clj
Expand Up @@ -37,3 +37,19 @@
q3 (quartile3 coll) q3 (quartile3 coll)
iqr (- q3 q1)] iqr (- q3 q1)]
(int (+ q3 (/ (* 3 iqr) 2))))) (int (+ q3 (/ (* 3 iqr) 2)))))

(defn- omitter-fn [to-omit]
(if (instance? java.util.regex.Pattern to-omit)
;; Curse you, non ifn regexes!
(comp (partial re-find to-omit) pr-str)
to-omit))

(defn omit-frames
"Remove frames matching to-omit, which can be a function or regex."
[trace-elems to-omit]
(if-let [omit? (omitter-fn to-omit)]
(reduce (fn [trace-elems elem]
(if (omit? elem)
trace-elems
(conj trace-elems elem))) [] trace-elems)
trace-elems))
11 changes: 5 additions & 6 deletions src/leiningen/hooks/clj_stacktrace_test.clj
Expand Up @@ -3,12 +3,11 @@
[robert.hooke :only [add-hook]])) [robert.hooke :only [add-hook]]))


(defn- hook-form [form project] (defn- hook-form [form project]
(let [pst (if (:test-color (:clj-stacktrace project)) `(do (alter-var-root (resolve '~'clojure.stacktrace/print-cause-trace)
'clj-stacktrace.repl/pst+ (constantly (fn [e#]
'clj-stacktrace.repl/pst)] (@(resolve '~'pst) e#
`(do (alter-var-root (resolve '~'clojure.stacktrace/print-cause-trace) ~(:clj-stacktrace project)))))
(constantly @(resolve '~pst))) ~form))
~form)))


(defn- add-stacktrace-hook [eval-in-project project form & [h s init]] (defn- add-stacktrace-hook [eval-in-project project form & [h s init]]
(eval-in-project project (hook-form form project) (eval-in-project project (hook-form form project)
Expand Down
37 changes: 28 additions & 9 deletions test/clj_stacktrace/repl_test.clj
@@ -1,7 +1,6 @@
(ns clj-stacktrace.repl-test (ns clj-stacktrace.repl-test
(:use clojure.test) (:use [clojure.test]
(:use clj-stacktrace.utils) [clj-stacktrace.repl]))
(:use clj-stacktrace.repl))


(defmacro with-cascading-exception (defmacro with-cascading-exception
"Execute body in the context of a variable bound to an exception instance "Execute body in the context of a variable bound to an exception instance
Expand All @@ -18,14 +17,34 @@
(binding [*e e] (binding [*e e]
(is (with-out-str (pst)))))) (is (with-out-str (pst))))))


(deftest test-pst-str
(with-cascading-exception e
(is (pst-str e))
(binding [*e e]
(is (pst-str)))))

(deftest test-pst+ (deftest test-pst+
(with-cascading-exception e (with-cascading-exception e
(is (with-out-str (pst+ e))) (is (with-out-str (pst+ e)))
(binding [*e e] (binding [*e e]
(is (with-out-str (pst+)))))) (is (with-out-str (pst+))))))

(deftest test-omit
(with-cascading-exception e
(is (not (re-find #"repl-test" (with-out-str
(pst e :omit #"repl-test")))))
(is (not (re-find #"Compiler.java"
(with-out-str
(pst e :omit (fn [e]
(= "Compiler.java" (:file e))))))))))

;; Color configuration tests

(defn starts-with? [color s]
(.startsWith s (color-codes color)))

(deftest pst-uses-no-color
(with-cascading-exception ex
(is (not (starts-with? :red (with-out-str (pst ex)))))))

(deftest defaults-to-red-exceptions
(with-cascading-exception ex
(is (starts-with? :red (with-out-str (pst+ ex))))))

(deftest configure-colors
(with-cascading-exception ex
(is (starts-with? :blue (with-out-str (pst+ ex :error-color :blue ))))))