Skip to content

Instantly share code, notes, and snippets.

@laurentpetit
Created March 17, 2011 22:20
Show Gist options
  • Save laurentpetit/875245 to your computer and use it in GitHub Desktop.
Save laurentpetit/875245 to your computer and use it in GitHub Desktop.
paredit.clj's core.clj modified content for smart indent "a la vim/emacs"
; todo
; done 1. emit text deltas, not plain text replacement (or IDEs will not like it)
; done 2. have a story for invalid parsetrees : just do nothing : currently = paredit deactivated if error from start-of-file to area of paredit's work
; 3. use restartable version of the parser
; 4. make paredit optional in ccw
; 5. prepare a new release of ccw
; 6. write with clojure.zip functions the close-* stuff
; 7. write the string related stuff
; ... ?
; . add support for more clojure-related source code ( #{}, #""... )
; ... and all the other paredit stuff ...
(ns paredit.core
(:use clojure.contrib.def)
(:use [paredit.parser :exclude [pts]])
(:use clojure.set)
(:use clojure.contrib.core)
(:require clojure.contrib.pprint)
(:require [clojure.contrib.str-utils2 :as str2])
(:require [paredit.text-utils :as t])
(:require [clojure.zip :as z])
(:use paredit.loc-utils)) ; TODO avoir un require :as l
#_(set! *warn-on-reflection* true)
;;; adaptable paredit configuration
(def ^String *newline* "\n")
;;; adaptable paredit configuration
(def *real-spaces* #{(str \newline) (str \tab) (str \space)})
(def *extended-spaces* (conj *real-spaces* (str \,)))
(def *open-brackets* (conj #{"(" "[" "{"} nil)) ; we add nil to the list to also match beginning of text
(def *close-brackets* (conj #{")" "]" "}"} nil)) ; we add nil to the list to also match end of text
(def *form-macro-chars* #{(str \#) (str \~) "~@" (str \') (str \`) (str \@) "^" "#'" "#_" "#!"})
(def *not-in-code* #{:string "\"\\" :comment :char :regex})
(defmacro with-memoized [func-names & body]
`(binding [~@(mapcat
(fn [func-name] [func-name `(memoize ~func-name)])
func-names)]
~@body))
(defmacro with-important-memoized [& body]
`(with-memoized
[start-offset
end-offset
loc-text
loc-col
loc-for-offset
leave-for-offset
loc-containing-offset
contains-offset?
normalized-selection
node-text]
~@body))
(defn normalized-selection
"makes a syntaxically correct selection, that is the returned nodes are siblings.
returns a vector of 2 locs.
If the selection is empty, the first loc will give the start (get it via a call to 'loc-start on it)
and the second loc will be nil.
If the selection is not empty, the second loc will give the end (get it via a call to 'loc-end on it).
Pre-requisites: length >=0, offset >=0. rloc = root loc of the tree"
[rloc offset length]
(let [left-leave (parse-leave (leave-for-offset rloc offset))
right-leave (parse-leave (leave-for-offset rloc (+ offset length)))
right-leave (cond
(= :root (loc-tag right-leave))
(parse-leave (leave-for-offset rloc (dec (+ offset length))))
(not= (+ offset length) (start-offset right-leave))
(parse-node right-leave)
(nil? (seq (previous-leaves right-leave)))
(parse-node right-leave)
:else
(parse-node (first (previous-leaves right-leave))))]
(if (or
(= [0 0] [offset length])
(and
(= 0 length)
(= (start-offset left-leave) offset))
(and
(= (start-offset (parse-node left-leave)) offset)
(= (end-offset (parse-node right-leave)) (+ offset length))
(same-parent? (parse-node left-leave) (parse-node right-leave))))
[left-leave (when-not (zero? length) right-leave)]
(let [left-leave (parse-node left-leave)
right-leave (parse-node right-leave)
min-depth (min (loc-depth left-leave) (loc-depth right-leave))
left-leave (up-to-depth left-leave min-depth)
right-leave (up-to-depth right-leave min-depth)]
(first
(filter
(fn [[l r]] (= (z/up l) (z/up r)))
(iterate
(fn [[l r]] [(z/up l) (z/up r)])
[left-leave right-leave])))))))
(defn parsed-in-tags?
[parsed tags-set]
(tags-set (-> parsed :parents peek :tag)))
(defn parse-stopped-in-code?
; TODO the current function is not general enough, it just works for the offset
; the parse stopped at
"true if character at offset offset is in a code
position, e.g. not in a string, regexp, literal char or comment"
[parsed]
(not (parsed-in-tags? parsed *not-in-code*)))
(defn in-code? [loc] (and loc (not (*not-in-code* (loc-tag (parse-node loc))))))
(defmulti paredit (fn [k & args] k))
(defn insert-balanced
[[o c] t chars-with-no-space-before chars-with-no-space-after]
(let [add-pre-space? (not (contains? chars-with-no-space-before
(t/previous-char-str t 1 #_(count o))))
add-post-space? (not (contains? chars-with-no-space-after
(t/next-char-str t)))
ins-str (str (if add-pre-space? " " "")
(str o c)
(if add-post-space? " " ""))
offset-shift (if add-post-space? -2 -1)]
(-> t (t/insert ins-str) (t/shift-offset offset-shift))))
(declare wrap-with-balanced)
(defn open-balanced
[parsed [o c] {:keys [^String text offset length] :as t}
chars-with-no-space-before chars-with-no-space-after]
(if (zero? length)
(let [offset-loc (-> parsed parsed-root-loc (loc-for-offset offset))]
(if (in-code? offset-loc)
(insert-balanced [o c] t chars-with-no-space-before chars-with-no-space-after)
(-> t (t/insert (str o)))))
(wrap-with-balanced parsed [o c] t)))
(defn close-balanced
[parsed [o c] {:keys [^String text offset length] :as t}
chars-with-no-space-before chars-with-no-space-after]
(let [offset-loc (-> parsed parsed-root-loc (loc-for-offset offset))]
(if (in-code? offset-loc)
(let [up-locs (take-while identity (iterate z/up offset-loc))
match (some #(when (= c (peek (:content (z/node %)))) %) up-locs)]
(if match
(let [last-loc (-> match z/down z/rightmost z/left)
nb-delete (if (= :whitespace (loc-tag last-loc))
(loc-count last-loc)
0)
t (if (> nb-delete 0)
(t/delete t (start-offset last-loc) nb-delete)
t)] ; z/left because there is the closing node
(-> t (t/set-offset (- (end-offset match) nb-delete))))
(-> t (t/insert (str c)))))
(-> t (t/insert (str c))))))
(defmethod paredit
:paredit-open-round
[cmd parsed {:keys [text offset length] :as t}]
(with-important-memoized
(open-balanced parsed ["(" ")"] t
(union (conj (into *real-spaces* *open-brackets*) "#") *form-macro-chars*)
(into *extended-spaces* *close-brackets*))))
(defmethod paredit
:paredit-open-square
[cmd parsed {:keys [text offset length] :as t}]
(with-important-memoized (open-balanced parsed ["[" "]"] t
(union (into *real-spaces* *open-brackets*) *form-macro-chars*)
(into *extended-spaces* *close-brackets*))))
(defmethod paredit
:paredit-open-curly
[cmd parsed {:keys [text offset length] :as t}]
(with-important-memoized (open-balanced parsed ["{" "}"] t
(union (conj (into *real-spaces* *open-brackets*) "#") *form-macro-chars*)
(into *extended-spaces* *close-brackets*))))
(defmethod paredit
:paredit-close-round
[cmd parsed {:keys [text offset length] :as t}]
(with-important-memoized (close-balanced parsed ["(" ")"] t
nil nil)))
(defmethod paredit
:paredit-close-square
[cmd parsed {:keys [text offset length] :as t}]
(with-important-memoized (close-balanced parsed ["[" "]"] t
nil nil)))
(defmethod paredit
:paredit-close-curly
[cmd parsed {:keys [text offset length] :as t}]
(with-important-memoized (close-balanced parsed ["{" "}"] t
nil nil)))
(defmethod paredit
:paredit-doublequote
[cmd parsed {:keys [text offset length] :as t}]
(with-important-memoized
(let [offset-loc (-> parsed parsed-root-loc (loc-for-offset offset))]
(cond
;(parse-stopped-in-code? parsed)
(in-code? offset-loc)
(insert-balanced [\" \"] t ; todo voir si on utilise open balanced ? (mais quid echappement?)
(conj (into *real-spaces* *open-brackets*) "#")
(into *extended-spaces* *close-brackets*))
(not= :string (loc-tag offset-loc))
(-> t (t/insert (str \")))
(and (= "\\" (t/previous-char-str t)) (not= "\\" (t/previous-char-str t 2)))
(-> t (t/insert (str \")))
(= "\"" (t/next-char-str t))
(t/shift-offset t 1)
:else
(-> t (t/insert (str \\ \")))))))
(defmethod paredit
:paredit-forward-delete
[cmd parsed {:keys [^String text offset length] :as t}]
(if (zero? (count text))
t
(with-important-memoized
(if parsed
(let [offset-loc (-> parsed parsed-root-loc (loc-for-offset offset))
handled-forms *brackets-tags*
in-handled-form (handled-forms (loc-tag offset-loc))
open-punct-length (.length (first (:content (z/node offset-loc))))]
(cond
(and in-handled-form (= offset (start-offset offset-loc)))
(t/shift-offset t open-punct-length)
(and in-handled-form (= offset (dec (end-offset offset-loc))))
(if (> (-> offset-loc z/node :content count) 2)
t ; don't move
(-> t ; delete the form
(t/delete (start-offset offset-loc) (loc-count offset-loc))
(t/shift-offset (- open-punct-length))))
:else
(t/delete t offset 1)))
(t/delete t offset 1)))))
(defmethod paredit
:paredit-backward-delete
[cmd parsed {:keys [^String text offset length] :as t}]
(if (zero? (count text))
t
(with-important-memoized
(if parsed
(let [offset (dec offset)
offset-loc (-> parsed parsed-root-loc (loc-for-offset offset))
;_ (println "offset-loc:" (z/node offset-loc))
handled-forms *brackets-tags*
in-handled-form (handled-forms (loc-tag offset-loc))
;_ (println "in-handled-form:" in-handled-form)
]
(cond
(and in-handled-form (<= (start-offset offset-loc) offset (+ (start-offset offset-loc) (dec (-> offset-loc z/down loc-count)))))
(if (> (-> offset-loc z/node :content count) 2)
t ; don't move
(do ;(println "delete the form:" (start-offset offset-loc) (loc-count offset-loc))
(-> t ; delete the form
(t/delete (start-offset offset-loc) (loc-count offset-loc))
(t/shift-offset (- (-> offset-loc z/down loc-count))))))
(and in-handled-form (= offset (dec (end-offset offset-loc))))
(do
;(println "final t:")
;(println (start-offset offset-loc) (loc-count offset-loc))
(t/shift-offset t -1))
:else
(-> t (t/delete offset 1) (t/shift-offset -1))))
(-> t (t/delete offset 1) (t/shift-offset -1))))))
(def lisp-forms (into #{} (map str '(let fn binding proxy reify extend extend-protocol extend-type bound-fn
if if-not if-let when when-not when-let when-first condp case loop dotimes
for while do doto try catch locking dosync doseq dorun doall
-> -?> >> future ns clojure.core/ns gen-class gen-interface))))
(defn ^{:doc "Returns logical true if the String probably names a special form or macro var"}
lisp-form? [s]
(or
(.startsWith s "def")
(.startsWith s "with")
(lisp-forms s)))
(defn indent-column
"pre-condition: line-offset is already the starting offset of a line"
[root-loc line-offset]
(let [loc (loc-for-offset root-loc (dec line-offset))]
(loop [loc (z/left loc) seen-loc nil indent 0]
#_(when (and seen-loc (= "if" (loc-text (first seen-loc)))) (println "seen-loc:" (second seen-loc)))
(cond
(nil? loc)
indent
(punct-loc? loc)
; we reached the start of the parent form, indent depending on the form's type
(if (#{"(" "#("} (loc-text loc))
(cond
(nil? seen-loc) (+ (loc-col loc) (loc-count loc) 1)
(lisp-form? (loc-text (first seen-loc))) (+ (loc-col loc) (loc-count loc) 1)
(second seen-loc) (loc-col (second seen-loc))
:else (+ (loc-col loc) (loc-count loc) 1))
(+ (loc-col loc) (loc-count loc)))
(= :whitespace (loc-tag loc))
; we see a space
(if (.contains ^String (loc-text loc) "\n")
(if seen-loc
(+ indent (dec (-> ^String (loc-text loc) (.substring (.lastIndexOf ^String (loc-text loc) "\n")) .length)))
(recur (z/left loc) nil 0))
(recur (z/left loc) seen-loc (+ indent (-> ^String (loc-text loc) .length))))
:else
(recur (z/left loc) (conj seen-loc loc) 0)))))
(defn text-selection
"returns a vector [offset length] from a normalized-selection"
[nsel]
(let [[l r] nsel
offset (start-offset l)
length (if (nil? r) 0 (- (end-offset r) offset))]
[offset length]))
(defn sel-match-normalized?
"Does the selection denoted by offset and length match l (left) and r (right) locs ?"
[offset length [l r]]
(if (zero? length)
(and (nil? r) (= offset (start-offset l)))
(and (= offset (start-offset l)) (= (+ offset length) (end-offset r)))))
(defmethod paredit
:paredit-expand-left
[cmd parsed {:keys [^String text offset length] :as t}]
(with-important-memoized (if-let [rloc (-?> parsed (parsed-root-loc true))]
(let [[l r] (normalized-selection rloc offset length)
l (if (sel-match-normalized? offset length [l r])
(if-let [nl (z/left l)] nl (if (punct-loc? l) (z/left (z/up l)) (z/up l)))
(do
[(z/node l) (and r (z/node r))]
l))
r (if (nil? r) l r)
[l r] (normalized-selection rloc (start-offset l) (- (end-offset r) (start-offset l)))]
(-> t (assoc-in [:offset] (start-offset l))
(assoc-in [:length] (if (nil? r) 0 (- (end-offset r) (start-offset l))))))
t)))
(defmethod paredit
:paredit-expand-up
[cmd parsed {:keys [^String text offset length] :as t}]
(with-important-memoized (if-let [rloc (-?> parsed (parsed-root-loc true))]
(let [[l r] (normalized-selection rloc offset length)]
(if-not (sel-match-normalized? offset length [l r])
(-> t (assoc-in [:offset] (start-offset l))
(assoc-in [:length] (if (nil? r) 0 (- (end-offset r) (start-offset l)))))
(let [l (if-let [nl (z/up (if (= offset (start-offset (parse-node l)))
(parse-node l)
(parse-leave l)))]
nl
l)]
(-> t (assoc-in [:offset] (start-offset l))
(assoc-in [:length] (- (end-offset l) (start-offset l)))))))
t)))
(defmethod paredit
:paredit-expand-right
[cmd parsed {:keys [^String text offset length] :as t}]
(with-important-memoized (if-let [rloc (-?> parsed (parsed-root-loc true))]
(let [[l r] (normalized-selection rloc offset length)]
(if-not (sel-match-normalized? offset length [l r])
(-> t (assoc-in [:offset] (start-offset l))
(assoc-in [:length] (if (nil? r) 0 (- (end-offset r) (start-offset l)))))
(let [r (if (nil? r)
l
(if-let [nr (z/right r)]
nr
(z/up r)))
[l r] (normalized-selection rloc (start-offset l) (- (end-offset r) (start-offset l)))]
(-> t (assoc-in [:offset] (start-offset l))
(assoc-in [:length] (if (nil? r) 0 (- (end-offset r) (start-offset l))))))))
t)))
(defmethod paredit
:paredit-raise-sexp
[cmd parsed {:keys [^String text offset length] :as t}]
(with-important-memoized (if-let [rloc (-?> parsed (parsed-root-loc true))]
(let [[l r] (normalized-selection rloc offset length)]
(if-not (and
(sel-match-normalized? offset length [l r])
(= offset (start-offset (parse-node l))))
t
(let
[to-raise-offset (start-offset l)
to-raise-length (- (if r (end-offset r) (end-offset (parse-node l))) (start-offset l))
to-raise-text (.substring text to-raise-offset (+ to-raise-offset to-raise-length))
l (if-let [nl (z/up (parse-node l))] nl l)
replace-offset (start-offset l)
replace-length (- (end-offset l) replace-offset)]
(-> t (assoc-in [:text] (t/str-replace text replace-offset replace-length to-raise-text))
(assoc-in [:offset] replace-offset)
(assoc-in [:length] 0)
(update-in [:modifs] conj {:offset replace-offset :length replace-length :text to-raise-text})))))
t)))
(defmethod paredit
:paredit-split-sexp
[cmd parsed {:keys [^String text offset length] :as t}]
(with-important-memoized (if (not= 0 length)
t
(if-let [rloc (-?> parsed (parsed-root-loc true))]
(let [[l r] (normalized-selection rloc offset length)
parent (cond
(= :string (loc-tag l)) l ; stay at the same level, and let the code take the correct open/close puncts, e.g. \" \"
:else (if-let [nl (z/up (if (start-punct? l) (parse-node l) (parse-leave l)))] nl (parse-leave l)))
open-punct (*tag-opening-brackets* (loc-tag parent))
close-punct ^String (*tag-closing-brackets* (loc-tag parent))]
(if-not close-punct
t
(let [replace-text (str close-punct " " open-punct)
[replace-offset
replace-length] (if (and
(not= :whitespace (loc-tag l))
(or
(= :string (loc-tag l))
(not (and
(sel-match-normalized? offset length [l r])
(= offset (start-offset (parse-node l)))))))
[offset 0]
(let [start (or (some #(when-not (= :whitespace (loc-tag %)) (end-offset %)) (previous-leaves l)) offset)
end (or (some #(when-not (= :whitespace (loc-tag %)) (start-offset %)) (next-leaves l)) 0)]
[start (- end start)]))
new-offset (+ replace-offset (.length close-punct))]
(-> t (assoc-in [:text] (t/str-replace text replace-offset replace-length replace-text))
(assoc-in [:offset] new-offset)
(update-in [:modifs] conj {:offset replace-offset :length replace-length :text replace-text})))))
t))))
(defmethod paredit
:paredit-join-sexps
[cmd parsed {:keys [^String text offset length] :as t}]
(with-important-memoized
(if (not= 0 length)
t
(if-let [rloc (-?> parsed (parsed-root-loc true))]
(let [[l _] (normalized-selection rloc offset length)
lf (first (remove #(= :whitespace (loc-tag %)) (previous-leaves l)))
rf (first (remove #(= :whitespace (loc-tag %)) (cons l (next-leaves l))))]
(if (or (nil? lf) (nil? rf) (start-punct? lf) (end-punct? rf))
t
(let [ln (parse-node lf)
rn (parse-node rf)]
(if-not (and
(= (loc-tag ln) (loc-tag rn)))
t
(let [replace-offset (- (end-offset ln) (if-let [punct ^String (*tag-closing-brackets* (loc-tag ln))] (.length punct) 0))
replace-length (- (+ (start-offset rn) (if-let [punct ^String (*tag-closing-brackets* (loc-tag rn))] (.length punct) 0)) replace-offset)
replace-text (if ((conj *atom* :string) (loc-tag ln)) "" " ")
new-offset (if (= offset (start-offset rn)) (+ replace-offset (.length replace-text)) replace-offset)]
(-> t (assoc-in [:text] (t/str-replace text replace-offset replace-length replace-text))
(assoc-in [:offset] new-offset)
(update-in [:modifs] conj {:offset replace-offset :length replace-length :text replace-text})))))))
t))))
(defn wrap-with-balanced
[parsed [^String o c] {:keys [^String text offset length] :as t}]
(let [bypass #(-> t
(update-in [:text] t/str-replace offset length o)
(update-in [:offset] + (.length o))
(assoc-in [:length] 0)
(update-in [:modifs] conj {:text o :offset offset :length length}))]
(if-let [rloc (-?> parsed (parsed-root-loc true))]
(let [left-leave (some (fn [l] (when (not= :whitespace (loc-tag l)) l)) (next-leaves (leave-for-offset rloc offset)))
right-leave (leave-for-offset rloc (+ offset (dec length))) ; may be a whitespace
right-leave (if (or (nil? right-leave) (<= (start-offset right-leave) (start-offset left-leave))) left-leave right-leave)]
(if (or
(not (in-code? (loc-containing-offset rloc offset)))
(not (in-code? (loc-containing-offset rloc (+ offset length))))
(> offset (start-offset left-leave))
(and (not= 0 length) (or (< (+ offset length) (end-offset right-leave))
(and (not= (z/up (loc-parse-node left-leave)) (z/up (loc-parse-node right-leave)))
(not (some #{(z/node (loc-parse-node left-leave))} (z/path right-leave)))))))
(bypass)
(let [text-to-wrap (.substring text (start-offset (z/up left-leave)) (end-offset (z/up right-leave)))
new-text (str o text-to-wrap c)
t (update-in t [:text] t/str-replace (start-offset left-leave) (.length text-to-wrap) new-text)
t (assoc-in t [:offset] (inc (start-offset left-leave)))]
(update-in t [:modifs] conj {:text new-text :offset (start-offset left-leave) :length (.length text-to-wrap)}))))
(bypass))))
(defmethod paredit
:paredit-wrap-square
[cmd parsed t]
(with-important-memoized (wrap-with-balanced parsed ["[" "]"] t)))
(defmethod paredit
:paredit-wrap-curly
[cmd parsed t]
(with-important-memoized (wrap-with-balanced parsed ["{" "}"] t)))
(defmethod paredit
:paredit-wrap-round
[cmd parsed t]
(with-important-memoized (wrap-with-balanced parsed ["(" ")"] t)))
(defmethod paredit
:paredit-newline
[cmd parsed {:keys [text offset length] :as t}]
; no call to with-important-memoized because we almost immediately delegate to :paredit-indent-line
(let [text (-> text (t/str-remove offset length) (t/str-insert offset "\n"))
r (paredit :paredit-indent-line
(parse text) ; TODO suppress (or optimize) this call, if possible
{:text text
:offset (inc offset)
:length 0
:modifs [{:text *newline* :offset offset :length length}]})]
(if (-?> r :modifs count (= 2))
(let [m1 (get-in r [:modifs 0])
m2 (get-in r [:modifs 1])
r (assoc-in r [:modifs] [{:text (str (:text m1) (:text m2)) :offset offset :length (+ (:length m1) (:length m2))}])
r (assoc-in r [:offset] (+ (.length ^String (get-in r [:modifs 0 :text])) offset))]
r)
r)))
(defmethod paredit
:paredit-indent-line
[cmd parsed {:keys [^String text offset length] :as t}]
(with-important-memoized
(if-let [rloc (-?> parsed (parsed-root-loc true))]
(let [line-start (t/line-start text offset)
line-stop (t/line-stop text offset)
loc (loc-for-offset rloc line-start)]
(if (and (= :string (loc-tag loc)) (< (start-offset loc) line-start))
t
(let [indent (indent-column rloc line-start)
cur-indent-col (-
(loop [o line-start]
(if (>= o (.length text))
o
(let [c (.charAt text o)]
(cond
(#{\return \newline} c) o ; test CR/LF before .isWhitespace !
(Character/isWhitespace c) (recur (inc o))
(= \, c) (recur (inc o))
:else o))))
line-start)
to-add (- indent cur-indent-col)]
(cond
(zero? to-add) t
:else (let [t (update-in t [:modifs] conj {:text (str2/repeat " " indent) :offset line-start :length cur-indent-col})
t (update-in t [:text] t/str-replace line-start cur-indent-col (str2/repeat " " indent))]
(cond
(>= offset (+ line-start cur-indent-col))
(update-in t [:offset] + to-add)
(<= offset (+ line-start indent))
t
:else
(update-in t [:offset] + (max to-add (- line-start
offset)))))))))
t)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment