diff --git a/core-text.cabal b/core-text.cabal
--- a/core-text.cabal
+++ b/core-text.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           core-text
-version:        0.3.5.0
+version:        0.3.7.0
 synopsis:       A rope type based on a finger tree over UTF-8 fragments
 description:    A rope data type for text, built as a finger tree over UTF-8 text
                 fragments. The package also includes utiltiy functions for breaking and
@@ -22,12 +22,12 @@
                 <https://github.com/aesiniath/unbeliever/blob/master/README.markdown README>
                 on GitHub.
 category:       System
-stability:      experimental
+stability:      stable
 homepage:       https://github.com/aesiniath/unbeliever#readme
 bug-reports:    https://github.com/aesiniath/unbeliever/issues
 author:         Andrew Cowie <istathar@gmail.com>
 maintainer:     Andrew Cowie <istathar@gmail.com>
-copyright:      © 2018-2021 Athae Eredh Siniath and Others
+copyright:      © 2018-2022 Athae Eredh Siniath and Others
 license:        MIT
 license-file:   LICENSE
 build-type:     Simple
diff --git a/lib/Core/Text/Breaking.hs b/lib/Core/Text/Breaking.hs
--- a/lib/Core/Text/Breaking.hs
+++ b/lib/Core/Text/Breaking.hs
@@ -3,6 +3,7 @@
 
 -- This is an Internal module, hidden from Haddock
 module Core.Text.Breaking (
+    breakRope,
     breakWords,
     breakLines,
     breakPieces,
@@ -142,3 +143,24 @@
      in if trailing
             then intoRope chunk : emptyRope : []
             else intoRope chunk : intoChunks predicate remainder'
+
+{-
+The utilities breakPieces and its helpers above were written long before this
+code. The special purpose functions above might have been written more easily
+if this below had been written first, but they _are_ special cases and they're
+done, so {shrug} if someone wants to unify these go right head, otherwise this
+can stand as almost but not-quite repetition.
+-}
+
+{- |
+Given a piece of 'Rope' and a predicate, break the text into two pieces at the first
+site where that predicate returns 'True'.
+
+@since 0.3.7
+-}
+breakRope :: (Char -> Bool) -> Rope -> (Rope, Rope)
+breakRope predicate text =
+    let possibleIndex = findIndexRope predicate text
+     in case possibleIndex of
+            Nothing -> (text, emptyRope)
+            Just i -> splitRope i text
diff --git a/lib/Core/Text/Rope.hs b/lib/Core/Text/Rope.hs
--- a/lib/Core/Text/Rope.hs
+++ b/lib/Core/Text/Rope.hs
@@ -79,6 +79,7 @@
     replicateRope,
     replicateChar,
     widthRope,
+    unconsRope,
     splitRope,
     takeRope,
     insertRope,
@@ -93,6 +94,7 @@
     unRope,
     nullRope,
     unsafeIntoRope,
+    copyRope,
     Width (..),
 ) where
 
@@ -137,7 +139,6 @@
     fromText,
     toLazyText,
  )
-import Prettyprinter (Pretty (..), emptyDoc)
 import qualified Data.Text.Short as S (
     ShortText,
     any,
@@ -154,10 +155,11 @@
     splitAt,
     toBuilder,
     toText,
-    unpack,
+    unpack, uncons
  )
 import qualified Data.Text.Short.Unsafe as S (fromByteStringUnsafe)
 import GHC.Generics (Generic)
+import Prettyprinter (Pretty (..), emptyDoc)
 import System.IO (Handle)
 
 {- |
@@ -291,7 +293,6 @@
 packRope :: String -> Rope
 packRope xs = Rope . F.singleton . S.pack $ xs
 
-
 {- |
 Repeat the input 'Rope' @n@ times. The follows the same semantics as other
 @replicate@ functions; if you ask for zero copies you'll get an empty text and
@@ -333,6 +334,24 @@
     (F.:<) piece _ -> S.null piece
 
 {- |
+Read the first character from a 'Rope', assuming it's length 1 or greater,
+returning 'Just' that character and the remainder of the text. Returns
+'Nothing' if the input is 0 length.
+
+@since 0.3.7
+-}
+unconsRope :: Rope -> Maybe (Char, Rope)
+unconsRope text =
+    let x = unRope text
+     in case F.viewl x of
+            F.EmptyL -> Nothing
+            (F.:<) piece x' ->
+                case S.uncons piece of
+                    Nothing -> Nothing
+                    Just (c, piece') -> Just (c, Rope ((F.<|) piece' x'))
+
+
+{- |
 Break the text into two pieces at the specified offset.
 
 Examples:
@@ -423,12 +442,36 @@
 -- corresponding tree.
 --
 instance Hashable Rope where
-    hashWithSalt salt (Rope x) = foldl' f salt x
-      where
-        f :: Int -> S.ShortText -> Int
-        f num piece = hashWithSalt num piece
+    hashWithSalt salt text =
+        let (Rope x') = copyRope text
+            piece = case F.viewl x' of
+                F.EmptyL -> S.empty
+                (F.:<) first _ -> first
+         in hashWithSalt salt piece
 
 {- |
+Copy the pieces underlying a 'Rope' into a single piece object.
+
+/Warning/
+
+This function was necessary to have a reliable 'Hashable' instance. Currently
+constructing this new @Rope@ is quite inefficient if the number of pieces or
+their respective lengths are large. Usually, however, we're calling 'hash' so
+the value can be used as a key in a hash table and such keys are typically
+simple (or at least not ridiculously long), so this is not an issue in normal
+usage.
+-}
+copyRope :: Rope -> Rope
+copyRope text@(Rope x) =
+    case F.viewl x of
+        F.EmptyL -> text
+        (F.:<) _ x' -> case F.viewl x' of
+            F.EmptyL -> text
+            -- TODO replace this with a function that allocates a ByteArray#
+            -- of the appropriate length then copies the pieces in
+            _ -> Rope (F.singleton (foldl' S.append S.empty x))
+
+{- |
 Machinery to interpret a type as containing valid Unicode that can be
 represented as a @Rope@ object.
 
@@ -457,10 +500,7 @@
     -- | Take another text-like type and convert it to a @Rope@.
     intoRope :: α -> Rope
 
-    -- | Append some text to this @Rope@. The default implementation is
-    -- basically a convenience wrapper around calling 'intoRope' and
-    -- 'mappend'ing it to your text (which will work just fine, but for some
-    -- types more efficient implementations are possible).
+    -- | Append some text to this @Rope@. The default implementation is basically a convenience wrapper around calling 'intoRope' and 'mappend'ing it to your text (which will work just fine, but for some types more efficient implementations are possible).
     appendRope :: α -> Rope -> Rope
     appendRope thing text = text <> intoRope thing
 
diff --git a/lib/Core/Text/Utilities.hs b/lib/Core/Text/Utilities.hs
--- a/lib/Core/Text/Utilities.hs
+++ b/lib/Core/Text/Utilities.hs
@@ -21,6 +21,7 @@
     -- * Helpers
     indefinite,
     oxford,
+    breakRope,
     breakWords,
     breakLines,
     breakPieces,
@@ -54,6 +55,7 @@
 import qualified Data.ByteString as B (ByteString, length, splitAt, unpack)
 import Data.Char (intToDigit)
 import qualified Data.FingerTree as F (ViewL (..), viewl, (<|))
+import Data.Kind (Type)
 import qualified Data.List as List (dropWhileEnd, foldl', splitAt)
 import qualified Data.Text as T
 import qualified Data.Text.Short as S (
@@ -98,7 +100,7 @@
 -}
 class Render α where
     -- | Which type are the annotations of your Doc going to be expressed in?
-    type Token α :: *
+    type Token α :: Type
 
     -- | Convert semantic tokens to specific ANSI escape tokens
     colourize :: Token α -> AnsiColour
@@ -291,7 +293,7 @@
 
 {- |
 Given a list of items (one word per Rope in the list) enumerate them with commas and
-an oxford comma before the last item. As you'd expect:
+an Oxford comma before the last item. As you'd expect:
 
 @
 λ> __oxford ["one", "two", "three"]__
