├── .gitignore
├── 472px-Ancientlibraryalex.jpg
├── LICENSE
├── README.md
├── project.clj
├── src
├── cljc
│ └── historian
│ │ └── core.cljc
└── cljs
│ └── historian
│ └── keys.cljs
└── test
└── historian
└── tests.clj
/.gitignore:
--------------------------------------------------------------------------------
1 | pom.xml
2 | *jar
3 | /lib/
4 | /classes/
5 | /targets/
6 | .lein-deps-sum
7 |
--------------------------------------------------------------------------------
/472px-Ancientlibraryalex.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reagent-project/historian/3fb87210fec4fe80a9ddaed10334d9f2b3463d65/472px-Ancientlibraryalex.jpg
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Eclipse Public License - v 1.0
2 |
3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
5 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
6 |
7 | 1. DEFINITIONS
8 |
9 | "Contribution" means:
10 |
11 | a) in the case of the initial Contributor, the initial code and documentation
12 | distributed under this Agreement, and
13 | b) in the case of each subsequent Contributor:
14 | i) changes to the Program, and
15 | ii) additions to the Program;
16 |
17 | where such changes and/or additions to the Program originate from and are
18 | distributed by that particular Contributor. A Contribution 'originates'
19 | from a Contributor if it was added to the Program by such Contributor
20 | itself or anyone acting on such Contributor's behalf. Contributions do not
21 | include additions to the Program which: (i) are separate modules of
22 | software distributed in conjunction with the Program under their own
23 | license agreement, and (ii) are not derivative works of the Program.
24 |
25 | "Contributor" means any person or entity that distributes the Program.
26 |
27 | "Licensed Patents" mean patent claims licensable by a Contributor which are
28 | necessarily infringed by the use or sale of its Contribution alone or when
29 | combined with the Program.
30 |
31 | "Program" means the Contributions distributed in accordance with this
32 | Agreement.
33 |
34 | "Recipient" means anyone who receives the Program under this Agreement,
35 | including all Contributors.
36 |
37 | 2. GRANT OF RIGHTS
38 | a) Subject to the terms of this Agreement, each Contributor hereby grants
39 | Recipient a non-exclusive, worldwide, royalty-free copyright license to
40 | reproduce, prepare derivative works of, publicly display, publicly
41 | perform, distribute and sublicense the Contribution of such Contributor,
42 | if any, and such derivative works, in source code and object code form.
43 | b) Subject to the terms of this Agreement, each Contributor hereby grants
44 | Recipient a non-exclusive, worldwide, royalty-free patent license under
45 | Licensed Patents to make, use, sell, offer to sell, import and otherwise
46 | transfer the Contribution of such Contributor, if any, in source code and
47 | object code form. This patent license shall apply to the combination of
48 | the Contribution and the Program if, at the time the Contribution is
49 | added by the Contributor, such addition of the Contribution causes such
50 | combination to be covered by the Licensed Patents. The patent license
51 | shall not apply to any other combinations which include the Contribution.
52 | No hardware per se is licensed hereunder.
53 | c) Recipient understands that although each Contributor grants the licenses
54 | to its Contributions set forth herein, no assurances are provided by any
55 | Contributor that the Program does not infringe the patent or other
56 | intellectual property rights of any other entity. Each Contributor
57 | disclaims any liability to Recipient for claims brought by any other
58 | entity based on infringement of intellectual property rights or
59 | otherwise. As a condition to exercising the rights and licenses granted
60 | hereunder, each Recipient hereby assumes sole responsibility to secure
61 | any other intellectual property rights needed, if any. For example, if a
62 | third party patent license is required to allow Recipient to distribute
63 | the Program, it is Recipient's responsibility to acquire that license
64 | before distributing the Program.
65 | d) Each Contributor represents that to its knowledge it has sufficient
66 | copyright rights in its Contribution, if any, to grant the copyright
67 | license set forth in this Agreement.
68 |
69 | 3. REQUIREMENTS
70 |
71 | A Contributor may choose to distribute the Program in object code form under
72 | its own license agreement, provided that:
73 |
74 | a) it complies with the terms and conditions of this Agreement; and
75 | b) its license agreement:
76 | i) effectively disclaims on behalf of all Contributors all warranties
77 | and conditions, express and implied, including warranties or
78 | conditions of title and non-infringement, and implied warranties or
79 | conditions of merchantability and fitness for a particular purpose;
80 | ii) effectively excludes on behalf of all Contributors all liability for
81 | damages, including direct, indirect, special, incidental and
82 | consequential damages, such as lost profits;
83 | iii) states that any provisions which differ from this Agreement are
84 | offered by that Contributor alone and not by any other party; and
85 | iv) states that source code for the Program is available from such
86 | Contributor, and informs licensees how to obtain it in a reasonable
87 | manner on or through a medium customarily used for software exchange.
88 |
89 | When the Program is made available in source code form:
90 |
91 | a) it must be made available under this Agreement; and
92 | b) a copy of this Agreement must be included with each copy of the Program.
93 | Contributors may not remove or alter any copyright notices contained
94 | within the Program.
95 |
96 | Each Contributor must identify itself as the originator of its Contribution,
97 | if
98 | any, in a manner that reasonably allows subsequent Recipients to identify the
99 | originator of the Contribution.
100 |
101 | 4. COMMERCIAL DISTRIBUTION
102 |
103 | Commercial distributors of software may accept certain responsibilities with
104 | respect to end users, business partners and the like. While this license is
105 | intended to facilitate the commercial use of the Program, the Contributor who
106 | includes the Program in a commercial product offering should do so in a manner
107 | which does not create potential liability for other Contributors. Therefore,
108 | if a Contributor includes the Program in a commercial product offering, such
109 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify
110 | every other Contributor ("Indemnified Contributor") against any losses,
111 | damages and costs (collectively "Losses") arising from claims, lawsuits and
112 | other legal actions brought by a third party against the Indemnified
113 | Contributor to the extent caused by the acts or omissions of such Commercial
114 | Contributor in connection with its distribution of the Program in a commercial
115 | product offering. The obligations in this section do not apply to any claims
116 | or Losses relating to any actual or alleged intellectual property
117 | infringement. In order to qualify, an Indemnified Contributor must:
118 | a) promptly notify the Commercial Contributor in writing of such claim, and
119 | b) allow the Commercial Contributor to control, and cooperate with the
120 | Commercial Contributor in, the defense and any related settlement
121 | negotiations. The Indemnified Contributor may participate in any such claim at
122 | its own expense.
123 |
124 | For example, a Contributor might include the Program in a commercial product
125 | offering, Product X. That Contributor is then a Commercial Contributor. If
126 | that Commercial Contributor then makes performance claims, or offers
127 | warranties related to Product X, those performance claims and warranties are
128 | such Commercial Contributor's responsibility alone. Under this section, the
129 | Commercial Contributor would have to defend claims against the other
130 | Contributors related to those performance claims and warranties, and if a
131 | court requires any other Contributor to pay any damages as a result, the
132 | Commercial Contributor must pay those damages.
133 |
134 | 5. NO WARRANTY
135 |
136 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN
137 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
138 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE,
139 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each
140 | Recipient is solely responsible for determining the appropriateness of using
141 | and distributing the Program and assumes all risks associated with its
142 | exercise of rights under this Agreement , including but not limited to the
143 | risks and costs of program errors, compliance with applicable laws, damage to
144 | or loss of data, programs or equipment, and unavailability or interruption of
145 | operations.
146 |
147 | 6. DISCLAIMER OF LIABILITY
148 |
149 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
150 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
151 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
152 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
153 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
154 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
155 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
156 | OF SUCH DAMAGES.
157 |
158 | 7. GENERAL
159 |
160 | If any provision of this Agreement is invalid or unenforceable under
161 | applicable law, it shall not affect the validity or enforceability of the
162 | remainder of the terms of this Agreement, and without further action by the
163 | parties hereto, such provision shall be reformed to the minimum extent
164 | necessary to make such provision valid and enforceable.
165 |
166 | If Recipient institutes patent litigation against any entity (including a
167 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself
168 | (excluding combinations of the Program with other software or hardware)
169 | infringes such Recipient's patent(s), then such Recipient's rights granted
170 | under Section 2(b) shall terminate as of the date such litigation is filed.
171 |
172 | All Recipient's rights under this Agreement shall terminate if it fails to
173 | comply with any of the material terms or conditions of this Agreement and does
174 | not cure such failure in a reasonable period of time after becoming aware of
175 | such noncompliance. If all Recipient's rights under this Agreement terminate,
176 | Recipient agrees to cease use and distribution of the Program as soon as
177 | reasonably practicable. However, Recipient's obligations under this Agreement
178 | and any licenses granted by Recipient relating to the Program shall continue
179 | and survive.
180 |
181 | Everyone is permitted to copy and distribute copies of this Agreement, but in
182 | order to avoid inconsistency the Agreement is copyrighted and may only be
183 | modified in the following manner. The Agreement Steward reserves the right to
184 | publish new versions (including revisions) of this Agreement from time to
185 | time. No one other than the Agreement Steward has the right to modify this
186 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The
187 | Eclipse Foundation may assign the responsibility to serve as the Agreement
188 | Steward to a suitable separate entity. Each new version of the Agreement will
189 | be given a distinguishing version number. The Program (including
190 | Contributions) may always be distributed subject to the version of the
191 | Agreement under which it was received. In addition, after a new version of the
192 | Agreement is published, Contributor may elect to distribute the Program
193 | (including its Contributions) under the new version. Except as expressly
194 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
195 | licenses to the intellectual property of any Contributor under this Agreement,
196 | whether expressly, by implication, estoppel or otherwise. All rights in the
197 | Program not expressly granted under this Agreement are reserved.
198 |
199 | This Agreement is governed by the laws of the State of New York and the
200 | intellectual property laws of the United States of America. No party to this
201 | Agreement will bring a legal action under this Agreement more than one year
202 | after the cause of action arose. Each party waives its rights to a jury trial in
203 | any resulting litigation.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Historian
2 | =========
3 |
4 |
6 |
7 | > "The present is the least important time we live in" --Alan Kay
8 |
9 | A drop-in atom-state-management (UNDOs!!) for your clojurescript projects.
10 |
11 | Also supports clojure in case you would want to make similar applications, or simply for testing.
12 |
13 |
14 | ## Table of contents
15 | [Usage](#usage)
16 | [Passive Atoms](#passive)
17 | [Shortcuts](#shortcuts)
18 | [Replacing Historian atoms](#atoms)
19 |
20 |
21 |
22 | ## Usage
23 |
24 | Add the following dependency in your `project.clj`:
25 |
26 | [](http://clojars.org/historian)
27 |
28 |
29 | And require historian in your namespace:
30 | ```clj
31 | (ns my-ns
32 | (:require [historian.core :as hist]))
33 | ```
34 |
35 | Suppose you have your state in an atom `my-state`:
36 | ```clj
37 | (def my-state (atom "ABC"))
38 | ```
39 |
40 | To keep an history of all changes, simply add your atom to historian:
41 |
42 | ```clj
43 | (hist/record! my-state :my-state)
44 |
45 | ;; then change the state of your atom
46 |
47 | (reset! my-state "DEF")
48 |
49 | @my-state
50 | => "DEF"
51 |
52 | (hist/undo!)
53 |
54 | @my-state
55 | => "ABC"
56 |
57 | (hist/redo!)
58 |
59 | @my-state
60 | => "DEF"
61 |
62 | ;; tada!
63 | ```
64 |
65 | Of course, sometimes we want to do some things without anyone noticing...
66 | ```clj
67 | ;; our current state is "ABC"
68 |
69 | (hist/off-the-record
70 | (reset! my-state "GHI")) ;; <--- this change won't be added to the undo history
71 |
72 | (reset! my-state "ZZZ")
73 |
74 | (hist/undo!)
75 |
76 | @my-state
77 | => "ABC"
78 | ```
79 |
80 | If you have a bunch of operations initiated by a single user action:
81 |
82 | ```clj
83 |
84 | (hist/with-single-record
85 | (doseq [i (range 200)]
86 | (reset! my-state i)))
87 | ;; We've just done 200 operations on the atom, but only the last state is recorded.
88 |
89 | (hist/undo!)
90 |
91 | @my-state
92 | => "ABC"
93 | ```
94 |
95 | You can also use the `with-single-before-and-after` macro to
96 | conditionally add a before AND after state when a non passive atom is
97 | modified. This is useful to snapshot the very last state of all
98 | passive atoms just before a normal atom is modified.
99 |
100 | To check if any undo/redo history is available, use `can-undo?` and `can-redo?`.
101 |
102 | When loading an app with multiple atoms, you should use `clear-history!` and `trigger-record!` to start with a clean slate.
103 |
104 |
105 |
106 | ## Passive Atoms
107 |
108 | When using `record!` on an atom, you can provide the optional
109 | `passive?` argument. A passive atom will *not* trigger any new save if
110 | modified. It will only be recorded if any other watched atom is
111 | modified."
112 |
113 | ```clj
114 | (hist/record! my-state :my-state :passive)
115 | ```
116 |
117 |
118 |
119 |
120 | ## Keyboard Shortcuts (cljs)
121 |
122 | You can bind `ctrl-z` and `ctrl-y` to undo and redo by using
123 | `bind-keys` in the `historian.keys` namespace.
124 |
125 |
126 |
127 |
128 | ## Replacing Atoms
129 |
130 | You might need to replace the atoms in which Historian stores its data.
131 | (Say, for example, to make them compatible with [Reagent] (https://github.com/holmsand/reagent)).
132 |
133 |
134 | ```clj
135 | (ns some-ns (:require [reagent.core :refer [atom]]
136 | [historian.core :as hist]))
137 |
138 | ;; for undos:
139 | (hist/replace-library! (atom [])) ; <----- the new atom must be a vector.
140 |
141 | ;; for redos:
142 | (hist/replace-prophecy! (atom [])) ; <----- the new atom must be a vector.
143 | ```
144 |
145 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject historian "1.2.2"
2 | :description "Automatically save atoms and restore their previous states if needed."
3 | :url "https://github.com/Frozenlock/historian"
4 | :scm {:name "git"
5 | :url "https://github.com/Frozenlock/historian"}
6 |
7 | :license {:name "Eclipse Public License - v 1.0"
8 | :url "http://www.eclipse.org/legal/epl-v10.html"
9 | :distribution :repo
10 | :comments "same as Clojure"}
11 |
12 | :dependencies [[org.clojure/clojure "1.9.0"]
13 | [org.clojure/clojurescript "1.10.238" :scope "provided"]]
14 |
15 | :source-paths ["src/cljc" "src/cljs"])
16 |
--------------------------------------------------------------------------------
/src/cljc/historian/core.cljc:
--------------------------------------------------------------------------------
1 | (ns ^{:doc "Manage states for your atoms. (Easy undo/redo)"
2 | :author "Frozenlock"
3 | :quote "The present is the least important time we live in. --Alan Kay"}
4 | historian.core
5 | #?(:cljs (:require-macros [historian.core :refer [off-the-record with-single-record with-single-before-and-after]])))
6 |
7 | (def alexandria
8 | "The great library... store your stuff here."
9 | (atom (atom [])))
10 |
11 | (defn get-library-atom []
12 | @alexandria)
13 |
14 | (def nostradamus
15 | "What will happen in the future..."
16 | (atom (atom [])))
17 |
18 | (defn get-prophecy-atom []
19 | @nostradamus)
20 |
21 | (def overseer
22 | "Who should we record?"
23 | (atom {}))
24 |
25 | (defn- register-atom!
26 | ([atom k] (register-atom! atom k nil))
27 | ([atom k passive?]
28 | (swap! overseer assoc k {:atom atom :passive? passive?})))
29 |
30 | (defn- de-register-atom! [k]
31 | (swap! overseer dissoc k))
32 |
33 | (defn- snapshot [k]
34 | (let [{:keys [atom passive?]} (get @overseer k)]
35 | {:value (deref atom)
36 | :passive? passive?
37 | :key k
38 | :timestamp
39 | #?(:cljs (goog.now)
40 | :clj (System/currentTimeMillis))}))
41 |
42 | (defn take-snapshots []
43 | (mapv snapshot (keys @overseer)))
44 |
45 | (defn different-from?
46 | "Check if any non-passive snapshot is different."
47 | [new old]
48 | (let [clean-maps #(when-not (:passive? %)
49 | (dissoc % :timestamp))]
50 | (not= (map clean-maps old)
51 | (map clean-maps new))))
52 |
53 | (defn- different-from-last? [new]
54 | (different-from? new (peek @(get-library-atom))))
55 |
56 | (defn- save-snapshots! [snaps]
57 | (swap! (get-library-atom) conj snaps))
58 |
59 | (defn- save-if-different! [snaps]
60 | (when (different-from-last? snaps)
61 | (save-snapshots! snaps)
62 | (reset! (get-prophecy-atom) [])))
63 |
64 | (defn- save-prophecies! [snaps]
65 | (swap! (get-prophecy-atom) conj snaps))
66 |
67 | (def ^:dynamic *record-active* true)
68 |
69 | (defn- restore!
70 | "Restore all the atoms being watched to a previous/different state."
71 | [snaps]
72 | (binding [*record-active* false]
73 | (doseq [s snaps]
74 | (reset! (get-in @overseer [(:key s) :atom])
75 | (:value s)))))
76 |
77 | (defn- watch! [atm]
78 | (add-watch atm ::historian-watch
79 | (fn [k _ old-value new-value]
80 | (when (not= old-value new-value) ;; really modified?
81 | (when *record-active*
82 | (save-if-different! (take-snapshots)))))))
83 |
84 | (defn- remove-watch! [a]
85 | (remove-watch a ::historian-watch))
86 |
87 | (defn- can-undo?* [records]
88 | (when (next records) true)) ;; because the CURRENT state is the
89 | ;; first in the list of states, we need
90 | ;; to have at least 2 (the current, plus
91 | ;; a previous one) to be able to undo.
92 |
93 | (defn- can-redo?* [records]
94 | (when (first records) true)) ;; contrary to undo, a single state is
95 | ;; enough to redo.
96 |
97 |
98 | ;;;; main API
99 |
100 |
101 | (defn trigger-record!
102 | "Trigger a record to history. The current state of at least one atom
103 | must be different from the previous one for the record to be
104 | included into history."[]
105 | (when *record-active*
106 | (save-if-different! (take-snapshots))))
107 |
108 | (defn overwrite-record!
109 | "Overwrite the last historic entry with a new one."
110 | ([] (overwrite-record! (take-snapshots)))
111 | ([snaps]
112 | (when *record-active*
113 | (swap! (get-library-atom) pop) ;; last snapshots
114 | (save-snapshots! snaps))))
115 |
116 | (defn replace-library!
117 | "The library atom (where all records are kept to enable 'undo') will
118 | be replaced by the new-atom. Useful if you've already done some
119 | modifications to the new-atom (like added some watchers). Depending
120 | on where you use this function, you might want to fire a
121 | `trigger-record!' just after.
122 |
123 | Usually, one would also want to use `replace-prophecy!' in order to
124 | replace the 'redo' atom."
125 | [new-atom]
126 | (reset! alexandria new-atom))
127 |
128 | (defn replace-prophecy!
129 | "The prophecy atom (where all records are kept to enable 'redo')
130 | will be replaced by the new-atom. Useful if you've already done some
131 | modifications to the new-atom (like added some watchers).
132 |
133 | Usually used with `replace-library'."
134 | [new-atom]
135 | (reset! nostradamus new-atom))
136 |
137 | (defn record!
138 | "Add the atom to the overseer watch. When any of the atom under its
139 | watch is modified, it triggers a save of every atom to history (if
140 | any of the atom is different form the last historic state). Return
141 | the atom.
142 |
143 | If `passive?' is true, the atom will NOT trigger any new save and
144 | will only be saved when another atom under watch is modified."
145 | ([atm k] (record! atm k nil))
146 | ([atm k passive?]
147 | (register-atom! atm k passive?)
148 | (when-not passive? (watch! atm))
149 | (trigger-record!)
150 | atm))
151 |
152 | (defn stop-record!
153 | "Remove the atom associated to this key from the overseer watch.
154 | This atom will no longer be watched, nor its state saved to
155 | history."[k]
156 | (remove-watch! (get-in @overseer [k :atom]))
157 | (de-register-atom! k))
158 |
159 | (defn stop-all-records!
160 | "Remove all the atoms from the overseer watch. The atoms will no
161 | longer be watched, nor any of their state saved to history."
162 | []
163 | (let [ks (keys @overseer)]
164 | (doseq [k ks]
165 | (stop-record! k))))
166 |
167 | (defn can-undo?
168 | "Do we have enough history to undo?"[]
169 | (can-undo?* @(get-library-atom)))
170 |
171 | (defn can-redo?
172 | "Can we redo?"[]
173 | (can-redo?* @(get-prophecy-atom)))
174 |
175 | (defmacro off-the-record
176 | "Temporarily deactivate the watches write to history."
177 | [& content]
178 | `(binding [*record-active* false]
179 | ~@content))
180 |
181 | (defn undo! []
182 | (let [alex @(get-library-atom)]
183 | (when (can-undo?* alex)
184 | (off-the-record
185 | (save-prophecies! (peek alex)) ;; add current state to the list
186 | ;; of 'redos'
187 | (->> alex
188 | pop ;; discard the current state
189 | (reset! (get-library-atom))
190 | peek
191 | restore!)))))
192 |
193 | (defn redo! []
194 | (let [nos @(get-prophecy-atom)]
195 | (when (can-redo?* nos)
196 | (off-the-record
197 | (save-snapshots! (peek nos)) ;; add the state as 'current' in
198 | ;; the undo atom.
199 | (reset! (get-prophecy-atom) (pop nos)) ;; Remove the prophecy
200 | (restore! (peek nos)))))) ;; Set the prophecy as the current state.
201 |
202 | (defn clear-history! []
203 | (reset! (get-library-atom) [])
204 | (reset! (get-prophecy-atom) []))
205 |
206 |
207 | (defmacro with-single-record
208 | "Temporarily deactivate the watches write to history. A single write
209 | is triggered at the end of the macro, assuming at least one of the
210 | atoms watched by the overseer has changed." [& content]
211 | `(do (off-the-record ~@content)
212 | (trigger-record!)))
213 |
214 |
215 | (defmacro with-single-before-and-after
216 | "Deactivate the watches write to history and execute the body. If
217 | any non-passive atom is modified, replace the last history with a
218 | snapshot taken just before executing the body and then take another
219 | snapshot." [& content]
220 | `(let [before-snaps# (take-snapshots)]
221 | (off-the-record ~@content)
222 | (let [after-snaps# (take-snapshots)]
223 | (when (different-from? after-snaps# before-snaps#)
224 | (overwrite-record! before-snaps#)
225 | (trigger-record!)))))
226 |
--------------------------------------------------------------------------------
/src/cljs/historian/keys.cljs:
--------------------------------------------------------------------------------
1 | (ns historian.keys
2 | (:require [historian.core :as hist]
3 | [goog.events :as events])
4 | (:import [goog.events EventType]))
5 |
6 |
7 | (defn bind-ctrl-z
8 | "Bind 'ctrl-z' to the undo function." []
9 | (events/listen js/window EventType.KEYDOWN
10 | #(when (and (= (.-keyCode %) 90) ;; 90 is Z
11 | (.-ctrlKey %))
12 | (hist/undo!))))
13 | (defn bind-ctrl-y
14 | "Bind 'ctrl-y' to the redo function." []
15 | (events/listen js/window EventType.KEYDOWN
16 | #(when (and (= (.-keyCode %) 89) ;; 89 is Y
17 | (.-ctrlKey %))
18 | (hist/redo!))))
19 |
20 | (defn bind-keys
21 | "Bind 'ctrl-z' and 'ctrl-y' to undo/redo."[]
22 | (bind-ctrl-z)
23 | (bind-ctrl-y))
24 |
--------------------------------------------------------------------------------
/test/historian/tests.clj:
--------------------------------------------------------------------------------
1 | (ns historian.tests
2 | (:require [clojure.test :as t :refer (is deftest with-test run-tests testing)]
3 | [historian.core :as hist]))
4 |
5 | (def test-atom (atom ""))
6 |
7 | (deftest undos-test
8 | (reset! test-atom "ABC")
9 | (is (= "ABC" @test-atom))
10 | (hist/record! test-atom :test-atom) ;; start the recording
11 | ;; at this point we should have taken the first record
12 | (reset! test-atom "DEF") ;; change the atom state, add new record
13 | (is (= "DEF" @test-atom))
14 | (hist/undo!) ;; restore the previous state
15 | (is (not= "DEF" @test-atom))
16 | (is (= "ABC" @test-atom))
17 | (reset! test-atom "GHI") ;; change the state
18 | (is (= "GHI" @test-atom))
19 | (hist/undo!) ;; now we should return to the previous state
20 | (is (= "ABC" @test-atom))
21 | (hist/stop-record! :test-atom)
22 | ;; we shouldn't be watching this atom anymore
23 | (let [nb (count (deref @hist/alexandria))]
24 | (reset! test-atom :new-value)
25 | (is (= nb (count (deref @hist/alexandria)))))
26 | (hist/clear-history!))
27 |
28 | (deftest with-single-record-test
29 | (hist/record! test-atom :test-atom)
30 | (reset! test-atom :before)
31 | (hist/with-single-record
32 | (doseq [i (range 10)]
33 | (reset! test-atom i)))
34 | (is (= 9 @test-atom))
35 | (hist/undo!)
36 | (is (= :before @test-atom))
37 | (hist/stop-record! :test-atom)
38 | (hist/clear-history!))
39 |
40 | (deftest redos-test
41 | (reset! test-atom :before)
42 | (hist/record! test-atom :test-atom) ;; first state
43 | (reset! test-atom :after) ;; second state
44 | (hist/undo!)
45 | (is (= :before @test-atom)) ;; back to first state
46 | (hist/redo!)
47 | (is (= :after @test-atom)) ;; now back to the second state
48 | (hist/undo!) ;; and again back to the first state
49 | (is (= :before @test-atom))
50 | (hist/stop-record! :test-atom)
51 | (hist/clear-history!))
52 |
--------------------------------------------------------------------------------