summaryrefslogtreecommitdiff
path: root/vim/bundle/slimv/swank-clojure/swank/core.clj
blob: 892b6a8040c4953608c1f962f8f56b104dd2e959 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
(ns swank.core
  (:use (swank util commands)
        (swank.util hooks)
        (swank.util.concurrent thread)
        (swank.core connection hooks threadmap))
  (:require (swank.util.concurrent [mbox :as mb])))

;; Protocol version
(defonce protocol-version (atom "20100404"))

;; Emacs packages
(def #^{:dynamic true} *current-package*)

;; current emacs eval id
(def #^{:dynamic true} *pending-continuations* '())

(def sldb-stepping-p nil)
(def sldb-initial-frames 10)
(def #^{:dynamic true} #^{:doc "The current level of recursive debugging."}
  *sldb-level* 0)
(def #^{:dynamic true} #^{:doc "The current restarts."}
  *sldb-restarts* 0)

(def #^{:doc "Include swank-clojure thread in stack trace for debugger."}
     debug-swank-clojure false)

(defonce active-threads (ref ()))

(defn maybe-ns [package]
  (cond
   (symbol? package) (or (find-ns package) (maybe-ns 'user))
   (string? package) (maybe-ns (symbol package))
   (keyword? package) (maybe-ns (name package))
   (instance? clojure.lang.Namespace package) package
   :else (maybe-ns 'user)))

(defmacro with-emacs-package [& body]
  `(binding [*ns* (maybe-ns *current-package*)]
     ~@body))

(defmacro with-package-tracking [& body]
  `(let [last-ns# *ns*]
     (try
      ~@body
      (finally
       (when-not (= last-ns# *ns*)
         (send-to-emacs `(:new-package ~(str (ns-name *ns*))
                                       ~(str (ns-name *ns*)))))))))

(defmacro dothread-swank [& body]
  `(dothread-keeping-clj [*current-connection*]
     ~@body))

;; Exceptions for debugging
(defonce debug-quit-exception (Exception. "Debug quit"))
(defonce debug-continue-exception (Exception. "Debug continue"))
(defonce debug-abort-exception (Exception. "Debug abort"))

(def #^{:dynamic true} #^Throwable *current-exception* nil)

;; Local environment
(def #^{:dynamic true} *current-env* nil)

(let [&env :unavailable]
  (defmacro local-bindings
    "Produces a map of the names of local bindings to their values."
    []
    (if-not (= &env :unavailable)
      (let [symbols (keys &env)]
        (zipmap (map (fn [sym] `(quote ~sym)) symbols) symbols)))))

;; Handle Evaluation
(defn send-to-emacs
    "Sends a message (msg) to emacs."
    ([msg]
       (mb/send @(*current-connection* :control-thread) msg)))

(defn send-repl-results-to-emacs [val]
  (send-to-emacs `(:write-string ~(str (pr-str val) "\n") :repl-result)))

(defn with-env-locals
  "Evals a form with given locals. The locals should be a map of symbols to
values."
  [form]
  (if (seq *current-env*)
    `(let ~(vec (mapcat #(list % `(*current-env* '~%)) (keys *current-env*)))
       ~form)
    form))

(defn eval-in-emacs-package [form]
  (with-emacs-package
   (eval form)))


(defn eval-from-control
  "Blocks for a mbox message from the control thread and executes it
   when received. The mbox message is expected to be a slime-fn."
  ([] (let [form (mb/receive (current-thread))]
        (apply (ns-resolve *ns* (first form)) (rest form)))))

(defn eval-loop
  "A loop which continuosly reads actions from the control thread and
   evaluates them (will block if no mbox message is available)."
  ([] (continuously (eval-from-control))))

(defn exception-causes [#^Throwable t]
  (lazy-seq
    (cons t (when-let [cause (.getCause t)]
              (exception-causes cause)))))

(defn- debug-quit-exception? [t]
  (some #(identical? debug-quit-exception %) (exception-causes t)))

(defn- debug-continue-exception? [t]
  (some #(identical? debug-continue-exception %) (exception-causes t)))

(defn- debug-abort-exception? [t]
  (some #(identical? debug-abort-exception %) (exception-causes t)))

(defn exception-stacktrace [t]
  (map #(list %1 %2 '(:restartable nil))
       (iterate inc 0)
       (map str (.getStackTrace t))))

(defn debugger-condition-for-emacs []
  (list (or (.getMessage *current-exception*) "No message.")
        (str "  [Thrown " (class *current-exception*) "]")
        nil))

(defn make-restart [kw name description f]
  [kw [name description f]])

(defn add-restart-if [condition restarts kw name description f]
  (if condition
    (conj restarts (make-restart kw name description f))
    restarts))

(declare sldb-debug)
(defn cause-restart-for [thrown depth]
  (make-restart
   (keyword (str "cause" depth))
   (str "CAUSE" depth)
   (str "Invoke debugger on cause "
        (apply str (take depth (repeat " ")))
        (.getMessage thrown)
        " [Thrown " (class thrown) "]")
   (partial sldb-debug nil thrown *pending-continuations*)))

(defn add-cause-restarts [restarts thrown]
  (loop [restarts restarts
         cause (.getCause thrown)
         level 1]
    (if cause
      (recur
       (conj restarts (cause-restart-for cause level))
       (.getCause cause)
       (inc level))
      restarts)))

(defn calculate-restarts [thrown]
  (let [restarts [(make-restart :quit "QUIT" "Quit to the SLIME top level"
                               (fn [] (throw debug-quit-exception)))]
        restarts (add-restart-if
                  (pos? *sldb-level*)
                  restarts
                  :abort "ABORT" (str "ABORT to SLIME level " (dec *sldb-level*))
                  (fn [] (throw debug-abort-exception)))
        restarts (add-restart-if
                  (and (.getMessage thrown)
                       (.contains (.getMessage thrown) "BREAK"))
                  restarts
                  :continue "CONTINUE" (str "Continue from breakpoint")
                  (fn [] (throw debug-continue-exception)))
        restarts (add-cause-restarts restarts thrown)]
    (into (array-map) restarts)))

(defn format-restarts-for-emacs []
  (doall (map #(list (first (second %)) (second (second %))) *sldb-restarts*)))

(defn build-backtrace [start end]
  (doall (take (- end start) (drop start (exception-stacktrace *current-exception*)))))

(defn build-debugger-info-for-emacs [start end]
  (list (debugger-condition-for-emacs)
        (format-restarts-for-emacs)
        (build-backtrace start end)
        *pending-continuations*))

(defn sldb-loop
  "A loop that is intented to take over an eval thread when a debug is
   encountered (an continue to perform the same thing). It will
   continue until a *debug-quit* exception is encountered."
  [level]
  (try
   (send-to-emacs
    (list* :debug (current-thread) level
           (build-debugger-info-for-emacs 0 sldb-initial-frames)))
   ([] (continuously
        (do
          (send-to-emacs `(:debug-activate ~(current-thread) ~level nil))
          (eval-from-control))))
   (catch Throwable t
     (send-to-emacs
      `(:debug-return ~(current-thread) ~*sldb-level* ~sldb-stepping-p))
     (if-not (debug-continue-exception? t)
       (throw t)))))

(defn invoke-debugger
  [locals #^Throwable thrown id]
  (binding [*current-env* locals
            *current-exception* thrown
            *sldb-restarts* (calculate-restarts thrown)
            *sldb-level* (inc *sldb-level*)]
    (sldb-loop *sldb-level*)))

(defn sldb-debug [locals thrown id]
  (try
   (invoke-debugger nil thrown id)
   (catch Throwable t
     (when (and (pos? *sldb-level*)
                (not (debug-abort-exception? t)))
       (throw t)))))

(defmacro break
  []
  `(invoke-debugger (local-bindings) (Exception. "BREAK:") *pending-continuations*))

(defn doall-seq [coll]
  (if (seq? coll)
    (doall coll)
    coll))

(defn eval-for-emacs [form buffer-package id]
  (try
   (binding [*current-package* buffer-package
             *pending-continuations* (cons id *pending-continuations*)]
     (if-let [f (slime-fn (first form))]
       (let [form (cons f (rest form))
             result (doall-seq (eval-in-emacs-package form))]
         (run-hook pre-reply-hook)
         (send-to-emacs `(:return ~(thread-name (current-thread))
                                  (:ok ~result) ~id)))
       ;; swank function not defined, abort
       (send-to-emacs `(:return ~(thread-name (current-thread)) (:abort) ~id))))
   (catch Throwable t
     ;; Thread/interrupted clears this thread's interrupted status; if
     ;; Thread.stop was called on us it may be set and will cause an
     ;; InterruptedException in one of the send-to-emacs calls below
     (Thread/interrupted)

     ;; (.printStackTrace t #^java.io.PrintWriter *err*)

     (cond
      (debug-quit-exception? t)
      (do
        (send-to-emacs `(:return ~(thread-name (current-thread)) (:abort) ~id))
        (if-not (zero? *sldb-level*)
          (throw t)))

      (debug-abort-exception? t)
      (do
        (send-to-emacs `(:return ~(thread-name (current-thread)) (:abort) ~id))
        (if-not (zero? *sldb-level*)
          (throw debug-abort-exception)))

      (debug-continue-exception? t)
      (do
        (send-to-emacs `(:return ~(thread-name (current-thread)) (:abort) ~id))
        (throw t))

      :else
      (do
        (set! *e t)
        (try
         (sldb-debug
          nil
          (if debug-swank-clojure t (or (.getCause t) t))
          id)
         ;; reply with abort
         (finally (send-to-emacs `(:return ~(thread-name (current-thread)) (:abort) ~id)))))))))

(defn- add-active-thread [thread]
  (dosync
   (commute active-threads conj thread)))

(defn- remove-active-thread [thread]
  (dosync
   (commute active-threads (fn [threads] (remove #(= % thread) threads)))))

(defn spawn-worker-thread
  "Spawn an thread that blocks for a single command from the control
   thread, executes it, then terminates."
  ([conn]
     (dothread-swank
       (try
        (add-active-thread (current-thread))
        (thread-set-name "Swank Worker Thread")
        (eval-from-control)
        (finally
         (remove-active-thread (current-thread)))))))

(defn spawn-repl-thread
  "Spawn an thread that sets itself as the current
   connection's :repl-thread and then enters an eval-loop"
  ([conn]
     (dothread-swank
      (thread-set-name "Swank REPL Thread")
      (with-connection conn
        (eval-loop)))))

(defn find-or-spawn-repl-thread
  "Returns the current connection's repl-thread or create a new one if
   the existing one does not exist."
  ([conn]
     ;; TODO - check if an existing repl-agent is still active & doesn't have errors
     (dosync
      (or (when-let [conn-repl-thread @(conn :repl-thread)]
            (when (.isAlive #^Thread conn-repl-thread)
              conn-repl-thread))
          (ref-set (conn :repl-thread)
                   (spawn-repl-thread conn))))))

(defn thread-for-evaluation
  "Given an id and connection, find or create the appropiate agent."
  ([id conn]
     (cond
      (= id true) (spawn-worker-thread conn)
      (= id :repl-thread) (find-or-spawn-repl-thread conn)
      :else (find-thread id))))

;; Handle control
(defn read-loop
  "A loop that reads from the socket (will block when no message
   available) and dispatches the message to the control thread."
  ([conn control]
     (with-connection conn
       (continuously (mb/send control (read-from-connection conn))))))

(defn dispatch-event
   "Dispatches/executes an event in the control thread's mailbox queue."
   ([ev conn]
      (let [[action & args] ev]
        (cond
         (= action :emacs-rex)
         (let [[form-string package thread id] args
               thread (thread-for-evaluation thread conn)]
           (mb/send thread `(eval-for-emacs ~form-string ~package ~id)))

         (= action :return)
         (let [[thread & ret] args]
           (binding [*print-level* nil, *print-length* nil]
             (write-to-connection conn `(:return ~@ret))))

         (one-of? action
                  :presentation-start :presentation-end
                  :new-package :new-features :ed :percent-apply
                  :indentation-update
                  :eval-no-wait :background-message :inspect)
         (binding [*print-level* nil, *print-length* nil]
           (write-to-connection conn ev))

         (= action :write-string)
         (write-to-connection conn ev)

         (one-of? action
                  :debug :debug-condition :debug-activate :debug-return)
         (let [[thread & args] args]
           (write-to-connection conn `(~action ~(thread-map-id thread) ~@args)))

         (= action :emacs-interrupt)
         (let [[thread & args] args]
           (dosync
            (cond
             (and (true? thread) (seq @active-threads))
             (.stop #^Thread (first @active-threads))
              (= thread :repl-thread) (.stop #^Thread @(conn :repl-thread)))))
         :else
         nil))))

;; Main loop definitions
(defn control-loop
  "A loop that reads from the mbox queue and runs dispatch-event on
   it (will block if no mbox control message is available). This is
   intended to only be run on the control thread."
  ([conn]
     (binding [*1 nil, *2 nil, *3 nil, *e nil]
       (with-connection conn
         (continuously (dispatch-event (mb/receive (current-thread)) conn))))))