Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@
[nrepl/drawbridge "0.3.0"]
[trptcolin/versioneer "0.2.0"]
[org.nrepl/incomplete "0.1.0"]
[org.clojars.trptcolin/sjacket "0.1.4"
:exclusions [org.clojure/clojure]]
;; bump transitive dep to avoid compatibility warning
[net.cgrand/parsley "0.9.3" :exclusions [org.clojure/clojure]]]
[org.clojure/tools.reader "1.5.0"]]
:min-lein-version "2.9.1"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
Expand Down
130 changes: 92 additions & 38 deletions src/reply/parsing.clj
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
(ns reply.parsing
(:require [net.cgrand.sjacket :as sjacket]
[net.cgrand.sjacket.parser :as sjacket.parser]))

(defn node-completed? [node]
(or (not= :net.cgrand.parsley/unfinished (:tag node))
(some #(= :net.cgrand.parsley/unexpected (:tag %))
(tree-seq :tag :content node))))
(:require [clojure.tools.reader :as r]
[clojure.tools.reader.reader-types :as rt]))

(defn subsequent-prompt-string [{:keys [prompt-string
subsequent-prompt-string]}]
Expand All @@ -15,37 +10,98 @@
\space)
"#_=> "))))

(defn remove-whitespace [forms]
(remove #(contains? #{:whitespace :comment :discard} (:tag %))
forms))
(defn- make-tracking-reader
"Create a tools.reader-compatible reader over `text` that tracks
its position via the `pos` atom. After each r/read call completes,
@pos is the offset of the next unread character in `text`."
[^String text pos]
(let [len (count text)]
(reify
rt/Reader
(read-char [_]
(let [p @pos]
(if (>= p len)
nil
(let [ch (.charAt text p)]
(swap! pos inc)
ch))))
(peek-char [_]
(let [p @pos]
(if (>= p len)
nil
(.charAt text p))))
rt/IPushbackReader
(unread [_ ch]
(when ch
(swap! pos dec))))))

(defn- try-read
"Attempt to read one form from `reader`.
Returns {:status :ok :val v}
or {:status :incomplete}
or {:status :error}
or {:status :eof}"
[reader]
(try
(let [val (binding [r/*read-eval* false]
(r/read {:eof ::eof :read-cond :allow :features #{:clj}}
reader))]
(if (= ::eof val)
{:status :eof}
{:status :ok :val val}))
(catch Exception e
(let [eof-incomplete?
(or (= :eof (:ex-kind (ex-data e)))
(let [cause (.getCause e)]
(and (= :reader-exception (:type (ex-data e)))
cause
(= :eof (:ex-kind (ex-data cause))))))]
(if eof-incomplete?
{:status :incomplete}
{:status :error})))))

(defn- read-forms
"Read all complete forms from `text` using tools.reader.
Returns [form-strings remaining-text] where remaining-text is the
incomplete trailing portion (or nil if everything was consumed)."
[text]
(let [pos (atom 0)
reader (make-tracking-reader text pos)]
(loop [forms []]
(let [start @pos
result (try-read reader)]
(case (:status result)
:ok (let [end @pos
form-str (.trim (subs text start end))]
(recur (conj forms form-str)))
:eof [forms nil]
:incomplete (let [remainder (.trim (subs text start))]
[forms remainder])
:error (let [end @pos
form-str (.trim (subs text start end))]
(if (.isEmpty form-str)
[forms nil]
(recur (conj forms form-str)))))))))

(defn reparse [text-so-far next-text]
(sjacket.parser/parser
(if text-so-far
(str text-so-far \newline next-text)
next-text)))

(declare parsed-forms)

(defn process-parse-tree [parse-tree options]
(let [complete-forms (take-while node-completed? (:content parse-tree))
remainder (drop-while node-completed? (:content parse-tree))
form-strings (map sjacket/str-pt
(remove-whitespace complete-forms))]
(cond
(seq remainder)
(lazy-seq
(concat form-strings
(parsed-forms
(assoc options
:text-so-far
(apply str (map sjacket/str-pt remainder))
:prompt-string
(subsequent-prompt-string options)))))
(seq form-strings)
(defn- process-input [text-so-far next-text options]
(let [text (if text-so-far
(str text-so-far \newline next-text)
next-text)
[form-strings remainder] (read-forms text)]
(if (and remainder (not (.isEmpty remainder)))
(lazy-seq
(concat form-strings
(parsed-forms
(assoc options
:text-so-far remainder
:prompt-string
(subsequent-prompt-string options)))))
(if (seq form-strings)
form-strings
:else
(list ""))))
(list "")))))

(defn parsed-forms
"Requires the following options:
Expand All @@ -60,9 +116,7 @@
(parsed-forms ((:read-line-fn options) options) options))
([next-text {:keys [request-exit text-so-far] :as options}]
(if next-text
(let [interrupted? (= :interrupted next-text)
parse-tree (when-not interrupted? (reparse text-so-far next-text))]
(if (or interrupted? (empty? (:content parse-tree)))
(list "")
(process-parse-tree parse-tree options)))
(if (= :interrupted next-text)
(list "")
(process-input text-so-far next-text options))
(list request-exit))))
22 changes: 21 additions & 1 deletion test/reply/parsing_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,28 @@
(with-in-str "\n\n"
(doall (p/parsed-forms options))))))

(t/testing "gets wht/testingespace"
(t/testing "gets whitespace"
(t/is (= [""]
(with-in-str " \n \n"
(doall (p/parsed-forms options))))))))

;; Syntax that was broken under sjacket/parsley (#172, #200)
(t/deftest modern-syntax
(let [eof (Object.)
read-line-fn (fn [state] (read-line))
options {:request-exit eof :read-line-fn read-line-fn}]

(t/testing "namespaced maps (#200)"
(t/is (= ["#::{:a 1}"]
(with-in-str "#::{:a 1}"
(doall (p/parsed-forms options))))))

(t/testing "tagged literals (#172)"
(t/is (= ["#inst \"2024-01-01\""]
(with-in-str "#inst \"2024-01-01\""
(doall (p/parsed-forms options))))))

(t/testing "reader conditionals"
(t/is (= ["#?(:clj 1 :cljs 2)"]
(with-in-str "#?(:clj 1 :cljs 2)"
(doall (p/parsed-forms options))))))))