Skip to content

Commit edadce3

Browse files
committed
Improved stacktrace formatting, closer match for clojure+
1 parent 0d0bbbf commit edadce3

File tree

6 files changed

+201
-93
lines changed

6 files changed

+201
-93
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### 4.5.0 - Jun 29, 2025
2+
3+
- Improved stacktrace formatting, closer match for clojure+
4+
15
### 4.4.3 - May 9, 2025
26

37
- Fixed `Align Cursors` not appearing in Command Palette #136

Clojure Sublimed Dark.sublime-color-scheme

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"background": "var(green-bg)"},
5252

5353
{"name": "Constants",
54-
"scope": "constant - constant.character.escape, punctuation.definition.constant, support.type",
54+
"scope": "constant - constant.character.escape, punctuation.definition.constant, support.type, source.sql keyword",
5555
"foreground": "var(magenta)"},
5656

5757
{"name": "Definitions",
@@ -67,7 +67,7 @@
6767
"foreground": "var(gray)"},
6868

6969
{"name": "Punctuation",
70-
"scope": "punctuation - punctuation.section - punctuation.definition",
70+
"scope": "punctuation - punctuation.section - punctuation.definition - punctuation.accessor",
7171
"foreground": "var(gray)"},
7272

7373
{"name": "Comma",

Clojure Sublimed Light.sublime-color-scheme

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"background": "#DBECB6"},
4444

4545
{"name": "Constants",
46-
"scope": "constant - constant.character.escape, punctuation.definition.constant, support.type",
46+
"scope": "constant - constant.character.escape, punctuation.definition.constant, support.type, source.sql keyword",
4747
"foreground": "#8a3eb5"},
4848

4949
{"name": "Definitions",
@@ -60,7 +60,7 @@
6060
"foreground": "var(gray)"},
6161

6262
{"name": "Punctuation",
63-
"scope": "punctuation - punctuation.section - punctuation.definition",
63+
"scope": "punctuation - punctuation.section - punctuation.definition - punctuation.accessor",
6464
"foreground": "var(gray)"},
6565

6666
{"name": "Comma",

src_clojure/clojure_sublimed/core.clj

Lines changed: 181 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
(ns clojure-sublimed.core
22
(:require
3-
[clojure.spec.alpha :as spec]
4-
[clojure.string :as str])
3+
[clojure.spec.alpha :as spec]
4+
[clojure.string :as str])
55
(:import
6-
[clojure.lang Compiler Compiler$CompilerException ExceptionInfo LispReader$ReaderException]
7-
[java.io BufferedWriter OutputStream OutputStreamWriter PrintWriter Writer]))
6+
[clojure.lang Compiler Compiler$CompilerException ExceptionInfo LispReader$ReaderException]
7+
[java.io BufferedWriter OutputStream OutputStreamWriter PrintWriter StringWriter Writer]))
88

99
(def ^:dynamic *print-quota*
1010
4096)
@@ -45,13 +45,13 @@
4545

4646
(defn bounded-pr-str [x]
4747
(let [writer (if (> *print-quota* 0)
48-
(bounded-writer (java.io.StringWriter.) *print-quota*)
49-
(java.io.StringWriter.))]
48+
(bounded-writer (StringWriter.) *print-quota*)
49+
(StringWriter.))]
5050
(try
5151
(binding [*out* writer]
5252
(pr x))
5353
(str writer)
54-
(catch clojure.lang.ExceptionInfo e
54+
(catch ExceptionInfo e
5555
(if (identical? quota-marker (ex-data e))
5656
(str writer "...")
5757
(throw e))))))
@@ -78,83 +78,189 @@
7878
(.append sb cbuf ^int off ^int len)))))]
7979
(PrintWriter. proxy true)))
8080

81-
;; CompilerException has location info, but its cause RuntimeException has the message ¯\_(ツ)_/¯
82-
(defn root-cause [^Throwable t]
83-
(loop [t t
84-
data nil]
85-
(if (and
86-
(nil? data)
87-
(or
88-
(instance? Compiler$CompilerException t)
89-
(instance? LispReader$ReaderException t))
90-
(not= [0 0] ((juxt :clojure.error/line :clojure.error/column) (ex-data t))))
91-
(recur t (ex-data t))
92-
(if-some [cause (some-> t .getCause)]
93-
(recur cause data)
94-
(if data
95-
(ExceptionInfo. "Wrapper to pass CompilerException ex-data" data t)
96-
t)))))
97-
98-
(defn duplicate? [^StackTraceElement prev-el ^StackTraceElement el]
81+
;; errors
82+
83+
(defn- noise? [^StackTraceElement el]
84+
(let [class (.getClassName el)]
85+
(#{"clojure.lang.RestFn" "clojure.lang.AFn"} class)))
86+
87+
(defn- duplicate? [^StackTraceElement prev-el ^StackTraceElement el]
9988
(and
10089
(= (.getClassName prev-el) (.getClassName el))
10190
(= (.getFileName prev-el) (.getFileName el))
102-
(= "invokeStatic" (.getMethodName prev-el))
103-
(#{"invoke" "doInvoke"} (.getMethodName el))))
91+
(#{"invokeStatic"} (.getMethodName prev-el))
92+
(#{"invoke" "doInvoke" "invokePrim"} (.getMethodName el))))
10493

105-
(defn clear-duplicates [els]
94+
(defn- clear-duplicates [els]
10695
(for [[prev-el el] (map vector (cons nil els) els)
10796
:when (or (nil? prev-el) (not (duplicate? prev-el el)))]
10897
el))
10998

110-
(defn trace-element [^StackTraceElement el]
99+
(defn- trace-element [^StackTraceElement el]
111100
(let [file (.getFileName el)
112-
clojure? (or (nil? file)
113-
(= file "NO_SOURCE_FILE")
114-
(.endsWith file ".clj")
115-
(.endsWith file ".cljc"))]
116-
{:method (if clojure?
117-
(Compiler/demunge (.getClassName el))
118-
(str (.getClassName el) "." (.getMethodName el)))
119-
:file (.getFileName el)
120-
:line (.getLineNumber el)}))
121-
122-
(defn as-table [table]
123-
(let [[method file] (for [col [:method :file]]
124-
(->> table
125-
(map #(get % col))
126-
(map str)
127-
(map count)
128-
(reduce max (count "null"))))
129-
format-str (str "\t%-" method "s\t%-" file "s\t:%d")]
130-
(->> table
131-
(map #(format format-str (:method %) (:file %) (:line %)))
132-
(str/join "\n"))))
133-
134-
(defn trace-str
135-
([t]
136-
(trace-str t nil))
137-
([^Throwable t opts]
138-
(let [{:clojure.error/keys [source line column]} (ex-data t)
139-
cause (or (.getCause t) t)]
140-
(str
141-
(->> (.getStackTrace cause)
142-
(take-while #(not (#{"clojure.lang.Compiler" "clojure.lang.LispReader"}
143-
(.getClassName ^StackTraceElement %))))
144-
(remove #(#{"clojure.lang.RestFn" "clojure.lang.AFn"} (.getClassName ^StackTraceElement %)))
145-
(clear-duplicates)
146-
(map trace-element)
147-
(reverse)
148-
(as-table))
149-
"\n>>> "
150-
(.getSimpleName (class cause))
151-
": "
152-
(.getMessage cause)
153-
(when (:location? opts true)
154-
(when (or source line column)
155-
(str " (" source ":" line ":" column ")")))
156-
(when-some [data (ex-data cause)]
157-
(str " " (bounded-pr-str data)))))))
101+
line (.getLineNumber el)
102+
cls (.getClassName el)
103+
method (.getMethodName el)
104+
clojure? (if file
105+
(or (.endsWith file ".clj") (.endsWith file ".cljc") (= file "NO_SOURCE_FILE"))
106+
(#{"invoke" "doInvoke" "invokePrim" "invokeStatic"} method))
107+
108+
[ns separator method]
109+
(cond
110+
(not clojure?)
111+
[(-> cls (str/split #"\.") last) "." method]
112+
113+
(#{"invoke" "doInvoke" "invokeStatic"} method)
114+
(let [[ns method] (str/split (Compiler/demunge cls) #"/" 2)
115+
method (-> method
116+
(str/replace #"eval\d{3,}" "eval")
117+
(str/replace #"--\d{3,}" ""))]
118+
[ns "/" method])
119+
120+
:else
121+
[(Compiler/demunge cls) "/" (Compiler/demunge method)])]
122+
{:element el
123+
:file (if (= "NO_SOURCE_FILE" file) nil file)
124+
:line line
125+
:ns ns
126+
:separator separator
127+
:method method}))
128+
129+
(defn- get-trace [^Throwable t]
130+
(->> (.getStackTrace t)
131+
(take-while
132+
(fn [^StackTraceElement el]
133+
(and
134+
(not= "clojure.lang.Compiler" (.getClassName el))
135+
(not= "clojure.lang.LispReader" (.getClassName el))
136+
(not (str/starts-with? (.getClassName el) "clojure_sublimed")))))
137+
(remove noise?)
138+
(clear-duplicates)
139+
(mapv trace-element)))
140+
141+
(defn datafy-throwable [^Throwable t]
142+
(let [trace (get-trace t)
143+
common (when-some [prev-t (.getCause t)]
144+
(let [prev-trace (get-trace prev-t)]
145+
(loop [m (dec (count trace))
146+
n (dec (count prev-trace))]
147+
(if (and (>= m 0) (>= n 0) (= (nth trace m) (nth prev-trace n)))
148+
(recur (dec m) (dec n))
149+
(- (dec (count trace)) m)))))]
150+
{:message (.getMessage t)
151+
:class (class t)
152+
:data (ex-data t)
153+
:trace trace
154+
:common (or common 0)
155+
:cause (some-> (.getCause t) datafy-throwable)}))
156+
157+
(defmacro write [w & args]
158+
(list* 'do
159+
(for [arg args]
160+
(if (or (string? arg) (= String (:tag (meta arg))))
161+
`(Writer/.write ~w ~arg)
162+
`(Writer/.write ~w (str ~arg))))))
163+
164+
(defn- pad [ch ^long len]
165+
(when (pos? len)
166+
(let [sb (StringBuilder. len)]
167+
(dotimes [_ len]
168+
(.append sb (char ch)))
169+
(str sb))))
170+
171+
(defn- split-file [s]
172+
(if-some [[_ name ext] (re-matches #"(.*)(\.[^.]+)" s)]
173+
[name ext]
174+
[s ""]))
175+
176+
(defn- linearize [key xs]
177+
(->> xs (iterate key) (take-while some?)))
178+
179+
(defn- longest-method [indent ts]
180+
(reduce max 0
181+
(for [[t depth] (map vector ts (range))
182+
el (:trace t)]
183+
(+ (* depth indent) (count (:ns el)) (count (:separator el)) (count (:method el))))))
184+
185+
(defn print-humanly [^Writer w ^Throwable t]
186+
(let [ts (linearize :cause (datafy-throwable t))
187+
max-len (longest-method 0 ts)
188+
indent " "]
189+
(doseq [[idx t] (map vector (range) ts)
190+
:let [{:keys [class message data trace common]} t]]
191+
;; class
192+
(write w (when (pos? idx) "\nCaused by: ") (.getSimpleName ^Class class))
193+
194+
;; message
195+
(when message
196+
(write w ": ")
197+
(print-method message w))
198+
199+
;; data
200+
(when data
201+
(write w " ")
202+
(print-method data w))
203+
204+
;; trace
205+
(doseq [el (drop-last common trace)
206+
:let [{:keys [ns separator method file line]} el
207+
right-pad (pad \space (- max-len (count ns) (count separator) (count method)))]]
208+
(write w "\n" indent)
209+
210+
;; method
211+
(write w ns separator method)
212+
213+
;; locaiton
214+
(cond
215+
(= -2 line)
216+
(write w right-pad " " "Native Method")
217+
218+
file
219+
(write w right-pad " " file " " line)))
220+
221+
;; ... common elements
222+
(when (pos? common)
223+
(write w "\n" indent "... " common " common elements")))))
224+
225+
(defn compiler-err-str [^Throwable t]
226+
(when (and
227+
(instance? Compiler$CompilerException t)
228+
(not (= :execution (:clojure.error/phase (ex-data t))))
229+
(str/starts-with? (.getMessage t) "Syntax error")
230+
(.getCause t)
231+
(instance? RuntimeException t))
232+
(let [cause (.getCause t)
233+
{:clojure.error/keys [source line column]} (ex-data t)
234+
source (some-> source (str/split #"/") last)]
235+
(str (.getMessage cause) " (" source ":" line ":" (some-> column inc) ")"))))
236+
237+
(defn root-cause ^Throwable [^Throwable t]
238+
(when t
239+
(if-some [cause (.getCause t)]
240+
(recur cause)
241+
t)))
242+
243+
(defn error-str [^Throwable t]
244+
(or
245+
(compiler-err-str t)
246+
(let [cause (root-cause t)
247+
data (ex-data cause)
248+
class (.getSimpleName (class cause))
249+
msg (.getMessage cause)]
250+
(cond-> (str class ": " msg)
251+
data (str " " (bounded-pr-str data))))))
252+
253+
(defn trace-str [^Throwable t]
254+
(or
255+
(compiler-err-str t)
256+
(let [w (StringWriter.)
257+
t (if (and
258+
(instance? Compiler$CompilerException t)
259+
(= :execution (:clojure.error/phase (ex-data t))))
260+
(.getCause t)
261+
t)]
262+
(print-humanly w t)
263+
(str w))))
158264

159265
;; Allow dynamic vars to be set in root thread when changed in spawned threads
160266

src_clojure/clojure_sublimed/socket_repl.clj

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,14 @@
3636
form))
3737

3838
(defn report-throwable [^Throwable t]
39-
(let [root ^Throwable (core/root-cause t)
40-
{:clojure.error/keys [source line column]} (ex-data root)
41-
cause ^Throwable (or (some-> root .getCause) root)
42-
data (ex-data cause)
43-
class (.getSimpleName (class cause))
44-
msg (.getMessage cause)
45-
val (cond-> (str class ": " msg)
46-
data
47-
(str " " (core/bounded-pr-str data)))
48-
trace (core/trace-str root {:location? false})]
39+
(let [{:clojure.error/keys [source line column]} (ex-data t)]
4940
(*out-fn*
5041
{"tag" "ex"
51-
"val" val
52-
"trace" trace
42+
"val" (core/error-str t)
43+
"trace" (core/trace-str t)
5344
"source" source
5445
"line" line
55-
"column" column})))
46+
"column" (some-> column inc)})))
5647

5748
(defn reader ^LineNumberingPushbackReader [code line column]
5849
(let [reader (LineNumberingPushbackReader. (StringReader. code))]

test_repl/error_undefined.cljc

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,11 @@
44
(let [x undefined-1
55
y undefined-2]))
66

7-
(defn h [])
7+
(defn h [])
8+
9+
(/ 1 0)
10+
11+
(defn d []
12+
(/ 1 0))
13+
14+
(d)

0 commit comments

Comments
 (0)