├── .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 | Historian logo 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 | [![Clojars Project](http://clojars.org/historian/latest-version.svg)](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 | --------------------------------------------------------------------------------