├── .ghci ├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGELOG ├── LICENSE ├── Setup.hs ├── bench └── MainBenchmarkSuite.hs ├── src └── Yi │ └── Rope.hs ├── test ├── Spec.hs └── Yi │ └── RopeSpec.hs └── yi-rope.cabal /.ghci: -------------------------------------------------------------------------------- 1 | :set -isrc -itest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.hi 3 | .stack-work/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | allow_failures: 3 | - env: CABALVER=head GHCVER=head 4 | 5 | env: 6 | - CABALVER=1.22 GHCVER=7.10.3 7 | - CABALVER=1.24 GHCVER=8.0.1 8 | - CABALVER=head GHCVER=head 9 | 10 | before_install: 11 | - sudo add-apt-repository -y ppa:hvr/ghc 12 | - sudo apt-get update 13 | - sudo apt-get install cabal-install-$CABALVER ghc-$GHCVER libicu-dev 14 | - export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:$PATH 15 | - cabal update 16 | - travis_retry cabal install --only-dependencies --enable-tests --enable-benchmarks 17 | 18 | install: 19 | - cabal install -j1 --enable-tests --enable-benchmarks 20 | 21 | script: 22 | - cabal test 23 | 24 | notifications: 25 | irc: "chat.freenode.net#yi" 26 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | yi-rope was originally designed and implemented in 2008 by 2 | Jean-Philippe Bernardy and Gustav Munkby as a fingertree containing 3 | utf8-encoded ByteStrings. It was ported to a fingertree over Text 4 | chunks in 2014 by Mateusz Kowalczyk. Below is the list of contributors 5 | to this package. If you have contributed to this package in any way, 6 | please add yourself to the list. 7 | 8 | Jean-Philippe Bernardy 9 | Gustav Munkby 10 | Mateusz Kowalczyk 11 | Jared Hance 12 | Siddhanathan Shanmugam 13 | Ryan Scott 14 | aiya000 15 | Dmitry Ivanov -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.11 (2017-06-06) 2 | ----------------- 3 | 4 | * Add Semigroup instances to support GHC 8.4 5 | 6 | 0.10 (2017-09-09) 7 | ----------------- 8 | 9 | * Dropped data-default, text-icu and charsdetect-ae dependencies 10 | * cons and snoc now don't produce chunks of unbounded length 11 | * Reverted lines' that was broken by 0.9 and added the corresponding test 12 | 13 | 0.9 (2017-07-03) 14 | ---------------- 15 | 16 | * Performance improvement for lines' 17 | 18 | 0.8 (2016-10-10) 19 | ---------------- 20 | 21 | * Fix Yi.Rope.all 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /bench/MainBenchmarkSuite.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -fno-warn-orphans #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | module Main where 4 | 5 | import Data.Char (isSpace) 6 | import Control.DeepSeq 7 | import Criterion.Main 8 | import qualified Criterion.Main as C 9 | import Data.Text (unlines, Text, replicate) 10 | import Prelude hiding (unlines) 11 | import qualified Yi.Rope as F 12 | 13 | 14 | longText :: Text 15 | longText = force . Data.Text.unlines 16 | $ Prelude.replicate 1000 "Lorem Спасибопожалусто dolor 中文測試 amet" 17 | {-# NOINLINE longText #-} 18 | 19 | longTextTree :: F.YiString 20 | longTextTree = force . F.fromText . Data.Text.unlines 21 | $ Prelude.replicate 1000 "Lorem Спасибопожалусто dolor 中文測試 amet" 22 | {-# NOINLINE longTextTree #-} 23 | 24 | longFRope :: F.YiString 25 | longFRope = force (F.fromText longText) 26 | {-# NOINLINE longFRope #-} 27 | 28 | wideText :: Text 29 | wideText = force . unlines 30 | $ Prelude.replicate 10 31 | $ Data.Text.replicate 100 "Lorem Спасибопожалусто dolor 中文測試 amet " 32 | {-# NOINLINE wideText #-} 33 | 34 | shortText :: Text 35 | shortText = force . unlines 36 | $ Prelude.replicate 3 "Lorem Спасибопожалусто dolor 中文測試 amet" 37 | {-# NOINLINE shortText #-} 38 | 39 | tinyText :: Text 40 | tinyText = force $ "Lorem Спасибопожалусто dolor 中文測試 amet" 41 | {-# NOINLINE tinyText #-} 42 | 43 | wideFRope :: F.YiString 44 | wideFRope = force (F.fromText wideText) 45 | {-# NOINLINE wideFRope #-} 46 | 47 | benchOnText :: NFData b => a -> String -> (a -> b) -> Benchmark 48 | benchOnText text name f 49 | = C.bench name 50 | $ C.nf f text 51 | 52 | benchSplitAt :: NFData a => a -> String 53 | -> (Int -> a -> (a, a)) 54 | -> C.Benchmark 55 | benchSplitAt text name f 56 | = C.bench name 57 | $ C.nf (\x -> Prelude.foldr ((fst .) . f) x [1000, 999 .. 1]) text 58 | 59 | benchTakeDrop :: NFData a => a -> String -> (Int -> a -> a) -> C.Benchmark 60 | benchTakeDrop text name f 61 | = C.bench name 62 | $ C.nf (\x -> foldr f x [1000, 999 .. 1]) text 63 | 64 | -- | Chunk sizes to test with. 65 | chunkSizes :: [Int] 66 | chunkSizes = [1200] 67 | 68 | wideTexts :: (Int -> String, [(Int, F.YiString)]) 69 | wideTexts = (\x -> "wide " ++ show x, mkTextSample wideText) 70 | 71 | longTexts :: (Int -> String, [(Int, F.YiString)]) 72 | longTexts = (\x -> "long " ++ show x, mkTextSample longText) 73 | 74 | shortTexts :: (Int -> [Char], [(Int, F.YiString)]) 75 | shortTexts = (\x -> "short " ++ show x, mkTextSample shortText) 76 | 77 | tinyTexts :: (Int -> String, [(Int, F.YiString)]) 78 | tinyTexts = (\x -> "tiny " ++ show x, mkTextSample tinyText) 79 | 80 | mkTextSample :: Text -> [(Int, F.YiString)] 81 | mkTextSample s = force $ zipWith mkTexts chunkSizes (Prelude.repeat s) 82 | where 83 | mkTexts :: Int -> Text -> (Int, F.YiString) 84 | mkTexts x t = (x, F.fromText' x t) 85 | 86 | allTexts :: [(Int -> String, [(Int, F.YiString)])] 87 | allTexts = [longTexts, wideTexts, shortTexts, tinyTexts] 88 | 89 | allChars :: [(Int -> String, [(Int, Char)])] 90 | allChars = map mkChar "λa" 91 | where 92 | mkChar c = (\x -> unwords [ "char", [c], show x ], [(1, c)]) 93 | 94 | -- | Sample usage: 95 | -- 96 | -- > mkGroup "drop" F.drop allTexts benchOnText 97 | mkGroup :: String -- ^ Group name 98 | -> f -- ^ Function being benchmarked 99 | -> [(chsize -> String, [(chsize, input)])] 100 | -> (input -> String -> f -> Benchmark) 101 | -> Benchmark 102 | mkGroup n f subs r = bgroup n tests 103 | where 104 | mkTest s (l, t) = r t (s l) f 105 | tests = Prelude.concat $ map (\(s, t) -> map (mkTest s) t) subs 106 | 107 | onTextGroup :: NFData a => String -> (F.YiString -> a) -> Benchmark 108 | onTextGroup n f = mkGroup n f allTexts benchOnText 109 | 110 | onCharGroup :: NFData a => String -> (Char -> a) -> Benchmark 111 | onCharGroup n f = mkGroup n f allChars benchOnText 112 | 113 | onIntGroup :: String -> (Int -> F.YiString -> F.YiString) -> Benchmark 114 | onIntGroup n f = mkGroup n f allTexts benchTakeDrop 115 | 116 | onSplitGroup :: String 117 | -> (Int -> F.YiString -> (F.YiString, F.YiString)) 118 | -> Benchmark 119 | onSplitGroup n f = mkGroup n f allTexts benchSplitAt 120 | 121 | 122 | splitBench :: [Benchmark] 123 | splitBench = 124 | [ onTextGroup "split none" (F.split (== '×')) 125 | , onTextGroup "split lots" (F.split (\x -> x == 'a' || x == 'o')) 126 | , onTextGroup "split all" (F.split (const True)) 127 | ] 128 | 129 | wordsBench :: [Benchmark] 130 | wordsBench = 131 | -- The replicate here inflates the benchmark like mad, should be 132 | -- moved out. 133 | [ onTextGroup "unwords" (\x -> F.unwords (Prelude.replicate 100 x)) 134 | , onTextGroup "words" F.words 135 | ] 136 | 137 | spanBreakBench :: [Benchmark] 138 | spanBreakBench = 139 | [ onTextGroup "spanTrue" $ F.span (const True) 140 | , onTextGroup "spanFalse" $ F.span (const False) 141 | , onTextGroup "spanSpace" $ F.span isSpace 142 | , onTextGroup "breakTrue" $ F.break (const True) 143 | , onTextGroup "breakFalse" $ F.break (const False) 144 | , onTextGroup "breakSpace" $ F.break isSpace 145 | ] 146 | 147 | foldBench :: [Benchmark] 148 | foldBench = 149 | [ onTextGroup "foldCount" $ F.foldl' (\x _ -> x + 1) (0 :: Integer) 150 | , onTextGroup "foldId" $ F.foldl' F.snoc F.empty 151 | , onTextGroup "foldReverse" $ F.foldl' (\x y -> F.cons y x) F.empty 152 | ] 153 | 154 | main :: IO () 155 | main = defaultMain $ 156 | [ onIntGroup "drop" F.drop 157 | , onIntGroup "take" F.take 158 | , onTextGroup "cons" (F.cons 'λ') 159 | , onTextGroup "snoc" (`F.snoc` 'λ') 160 | , onCharGroup "singleton" F.singleton 161 | , onTextGroup "countNewLines" F.countNewLines 162 | , onTextGroup "lines" F.lines 163 | , onTextGroup "lines'" F.lines' 164 | , onSplitGroup "splitAt" F.splitAt 165 | , onSplitGroup "splitAtLine" F.splitAtLine 166 | , onTextGroup "toReverseString" F.toReverseString 167 | , onTextGroup "toReverseText" F.toReverseText 168 | , onTextGroup "toText" F.toText 169 | , onTextGroup "length" F.length 170 | , onTextGroup "reverse" F.reverse 171 | , onTextGroup "null" F.null 172 | , onTextGroup "empty" $ const F.empty 173 | , onTextGroup "append" (\x -> F.append x x) 174 | , onTextGroup "concat x100" $ F.concat . Prelude.replicate 100 175 | , onTextGroup "any OK, (== '中')" $ F.any (== '中') 176 | , onTextGroup "any bad, (== '×')" $ F.any (== '×') 177 | , onTextGroup "all OK (/= '×')" $ F.all (== '×') 178 | , onTextGroup "all bad, (== '中')" $ F.all (== '中') 179 | , onTextGroup "init" F.init 180 | , onTextGroup "tail" F.tail 181 | , onTextGroup "replicate 50" (F.replicate 50) 182 | ] ++ splitBench 183 | ++ wordsBench 184 | ++ spanBreakBench 185 | ++ foldBench -------------------------------------------------------------------------------- /src/Yi/Rope.hs: -------------------------------------------------------------------------------- 1 | {-# language CPP #-} 2 | {-# language BangPatterns #-} 3 | {-# language DeriveDataTypeable #-} 4 | {-# language LambdaCase #-} 5 | {-# language MultiParamTypeClasses #-} 6 | {-# language OverloadedStrings #-} 7 | {-# language ScopedTypeVariables #-} 8 | {-# language ViewPatterns #-} 9 | {-# options_haddock show-extensions #-} 10 | 11 | -- | 12 | -- Module : Yi.Rope 13 | -- License : GPL-2 14 | -- Maintainer : yi-devel@googlegroups.com 15 | -- Stability : experimental 16 | -- Portability : portable 17 | -- 18 | -- This module defines a @rope@ data structure for use in Yi. This 19 | -- specific implementation uses a fingertree over Text. 20 | -- 21 | -- In contrast to our old implementation, we can now reap all the 22 | -- benefits of Text: automatic unicode handling and blazing fast 23 | -- implementation on underlying strings. This frees us from a lot of 24 | -- book-keeping. We don't lose out on not using ByteString directly 25 | -- because the old implementation encoded it into UTF8 anyway, making 26 | -- it unsuitable for storing anything but text. 27 | 28 | module Yi.Rope ( 29 | Yi.Rope.YiString, 30 | 31 | -- * Conversions to YiString 32 | Yi.Rope.fromString, Yi.Rope.fromText, 33 | Yi.Rope.fromString', Yi.Rope.fromText', 34 | 35 | -- * Conversions from YiString 36 | Yi.Rope.toString, Yi.Rope.toReverseString, 37 | Yi.Rope.toText, Yi.Rope.toReverseText, 38 | 39 | -- * Functions over content 40 | Yi.Rope.null, Yi.Rope.empty, Yi.Rope.take, Yi.Rope.drop, 41 | Yi.Rope.length, Yi.Rope.reverse, Yi.Rope.countNewLines, 42 | Yi.Rope.lines, Yi.Rope.lines', Yi.Rope.unlines, 43 | Yi.Rope.splitAt, Yi.Rope.splitAtLine, 44 | Yi.Rope.cons, Yi.Rope.snoc, Yi.Rope.singleton, 45 | Yi.Rope.head, Yi.Rope.last, 46 | Yi.Rope.append, Yi.Rope.concat, 47 | Yi.Rope.any, Yi.Rope.all, 48 | Yi.Rope.dropWhile, Yi.Rope.takeWhile, 49 | Yi.Rope.dropWhileEnd, Yi.Rope.takeWhileEnd, 50 | Yi.Rope.intercalate, Yi.Rope.intersperse, 51 | Yi.Rope.filter, Yi.Rope.map, 52 | Yi.Rope.words, Yi.Rope.unwords, 53 | Yi.Rope.split, Yi.Rope.init, Yi.Rope.tail, 54 | Yi.Rope.span, Yi.Rope.break, Yi.Rope.foldl', 55 | Yi.Rope.replicate, Yi.Rope.replicateChar, 56 | 57 | -- * IO 58 | Yi.Rope.readFile, Yi.Rope.writeFile, 59 | 60 | -- * Escape latches to underlying content. Note that these are safe 61 | -- to use but it does not mean they should. 62 | Yi.Rope.fromRope, Yi.Rope.withText, Yi.Rope.unsafeWithText 63 | 64 | ) where 65 | 66 | import Control.DeepSeq 67 | import Control.Exception (try) 68 | import Data.Binary 69 | import qualified Data.ByteString.Lazy as BSL 70 | import Data.Char (isSpace) 71 | import qualified Data.FingerTree as T 72 | import Data.FingerTree hiding (null, empty, reverse, split) 73 | import qualified Data.List as L (foldl') 74 | import Data.Maybe 75 | import Data.Monoid 76 | import Data.String (IsString(..)) 77 | import qualified Data.Text as TX 78 | import qualified Data.Text.Encoding.Error as TXEE 79 | import qualified Data.Text.Lazy as TXL 80 | import qualified Data.Text.Lazy.Encoding as TXLE 81 | import qualified Data.Text.IO as TXIO (writeFile) 82 | import Data.Typeable 83 | import Prelude hiding (drop) 84 | 85 | -- | Used to cache the size of the strings. 86 | data Size = Indices { charIndex :: {-# UNPACK #-} !Int 87 | -- ^ How many characters under here? 88 | , lineIndex :: Int 89 | -- ^ How many lines under here? 90 | } deriving (Eq, Show, Typeable) 91 | 92 | -- | A chunk storing the string of the type it is indexed by. It 93 | -- caches the length of stored string. 94 | data YiChunk = Chunk { chunkSize :: {-# UNPACK #-} !Int 95 | , _fromChunk :: {-# UNPACK #-} !TX.Text 96 | } deriving (Show, Eq, Typeable) 97 | 98 | -- | Makes a chunk from a given string. We allow for an arbitrary 99 | -- length function here to allow us to bypass the calculation with 100 | -- 'const' in case the length is known ahead of time. In most cases, 101 | -- the use of this is 102 | -- 103 | -- > mkChunk 'TX.Text.length' someText 104 | mkChunk :: (TX.Text -> Int) -- ^ The length function to use. 105 | -> TX.Text 106 | -> YiChunk 107 | mkChunk l t = Chunk (l t) t 108 | 109 | -- | Transform the chunk content. It's vital that the transformation 110 | -- preserves the length of the content. 111 | overChunk :: (TX.Text -> TX.Text) -- ^ Length-preserving content transformation. 112 | -> YiChunk -> YiChunk 113 | overChunk f (Chunk l t) = Chunk l (f t) 114 | 115 | -- | Counts number of newlines in the given 'TX.Text'. 116 | countNl :: TX.Text -> Int 117 | countNl = TX.count "\n" 118 | 119 | #if __GLASGOW_HASKELL__ >= 804 120 | instance Semigroup Size where 121 | (<>) = mappend 122 | #endif 123 | 124 | instance Monoid Size where 125 | mempty = Indices 0 0 126 | Indices c l `mappend` Indices c' l' = Indices (c + c') (l + l') 127 | 128 | instance Measured Size YiChunk where 129 | measure (Chunk l t) = Indices l (countNl t) 130 | 131 | -- | A 'YiString' is a 'FingerTree' with cached char and line counts 132 | -- over chunks of 'TX.Text'. 133 | newtype YiString = YiString { fromRope :: FingerTree Size YiChunk } 134 | deriving (Show, Typeable) 135 | 136 | -- | Two 'YiString's are equal if their underlying text is. 137 | -- 138 | -- Implementation note: This just uses 'TX.Text' equality as there is 139 | -- no real opportunity for optimisation here except for a cached 140 | -- length check first. We could unroll the trees and mess around with 141 | -- matching prefixes but the overhead would be higher than a simple 142 | -- conversion and relying on GHC optimisation. 143 | -- 144 | -- The derived Eq implementation for the underlying tree only passes 145 | -- the equality check if the chunks are the same too which is not what 146 | -- we want. 147 | instance Eq YiString where 148 | t == t' = Yi.Rope.length t == Yi.Rope.length t' && toText t == toText t' 149 | 150 | instance NFData Size where 151 | rnf (Indices !c !l) = c `seq` l `seq` () 152 | 153 | instance NFData YiChunk where 154 | rnf (Chunk !i !t) = i `seq` rnf t 155 | 156 | instance NFData YiString where 157 | rnf = rnf . toText 158 | 159 | instance IsString YiString where 160 | fromString = Yi.Rope.fromString 161 | 162 | #if __GLASGOW_HASKELL__ >= 804 163 | instance Semigroup YiString where 164 | (<>) = mappend 165 | #endif 166 | 167 | instance Monoid YiString where 168 | mempty = Yi.Rope.empty 169 | mappend = Yi.Rope.append 170 | mconcat = Yi.Rope.concat 171 | 172 | instance Ord YiString where 173 | compare x y = toText x `compare` toText y 174 | 175 | (-|) :: YiChunk -> FingerTree Size YiChunk -> FingerTree Size YiChunk 176 | b -| t | chunkSize b == 0 = t 177 | | otherwise = b <| t 178 | 179 | (|-) :: FingerTree Size YiChunk -> YiChunk -> FingerTree Size YiChunk 180 | t |- b | chunkSize b == 0 = t 181 | | otherwise = t |> b 182 | 183 | -- | Default size chunk to use. Currently @1200@ as this is what 184 | -- benchmarks suggest. 185 | -- 186 | -- This makes the biggest difference with 'lines'-like and 187 | -- 'concat'-like functions. Bigger chunks make 'concat' (much) faster 188 | -- but 'lines' slower. In general it seems that we benefit more from 189 | -- larger chunks and 1200 seems to be the sweet spot. 190 | defaultChunkSize :: Int 191 | defaultChunkSize = 1200 192 | 193 | -- | Reverse the whole underlying string. 194 | -- 195 | -- This involves reversing the order of the chunks as well as content 196 | -- of the chunks. We use a little optimisation here that re-uses the 197 | -- content of each chunk but this exposes a potential problem: after 198 | -- many transformations, our chunks size might become quite varied 199 | -- (but never more than the default size), perhaps we should 200 | -- periodically rechunk the tree to recover nice sizes? 201 | reverse :: YiString -> YiString 202 | reverse = YiString . fmap' (overChunk TX.reverse) . T.reverse . fromRope 203 | 204 | -- | See 'fromText'. 205 | fromString :: String -> YiString 206 | fromString = fromText . TX.pack 207 | 208 | -- | See 'fromText''. 209 | fromString' :: Int -> String -> YiString 210 | fromString' n = fromText' n . TX.pack 211 | 212 | -- | See 'toText'. 213 | toString :: YiString -> String 214 | toString = TX.unpack . toText 215 | 216 | -- | See 'toReverseText'. 217 | -- 218 | -- Note that it is actually ~4.5 times faster to use 'toReverseText' 219 | -- and unpack the result than to convert to 'String' and use 220 | -- 'Prelude.reverse'. 221 | toReverseString :: YiString -> String 222 | toReverseString = TX.unpack . toReverseText 223 | 224 | -- | This is like 'fromText' but it allows the user to specify the 225 | -- chunk size to be used. Uses 'defaultChunkSize' if the given 226 | -- size is <= 0. 227 | fromText' :: Int -> TX.Text -> YiString 228 | fromText' n | n <= 0 = fromText' defaultChunkSize 229 | | otherwise = YiString . r T.empty . f 230 | where 231 | f = TX.chunksOf n 232 | 233 | -- Convert the given string into chunks in the tree. We have a 234 | -- special case for a single element case: because we split on 235 | -- predetermined chunk size, we know that all chunks but the last 236 | -- one will be the specified size so we can optimise here instead 237 | -- of having to recompute chunk size at creation. 238 | r :: FingerTree Size YiChunk -> [TX.Text] -> FingerTree Size YiChunk 239 | r !tr [] = tr 240 | r !tr (t:[]) = tr |- mkChunk TX.length t 241 | r !tr (t:ts) = let r' = tr |- mkChunk (const n) t 242 | in r r' ts 243 | 244 | -- | Converts a 'TX.Text' into a 'YiString' using 245 | -- 'defaultChunkSize'-sized chunks for the underlying tree. 246 | fromText :: TX.Text -> YiString 247 | fromText = fromText' defaultChunkSize 248 | 249 | fromLazyText :: TXL.Text -> YiString 250 | fromLazyText = YiString . T.fromList . fmap (mkChunk TX.length) . TXL.toChunks 251 | 252 | -- | Consider whether you really need to use this! 253 | toText :: YiString -> TX.Text 254 | toText = TX.concat . go . fromRope 255 | where 256 | go :: FingerTree Size YiChunk -> [TX.Text] 257 | go t = case viewl t of 258 | Chunk _ !c :< cs -> c : go cs 259 | EmptyL -> [] 260 | 261 | -- | Spits out the underlying string, reversed. 262 | -- 263 | -- Note that this is actually slightly faster than manually unrolling 264 | -- the tree from the end, 'TX.reverse'ing each chunk and 265 | -- 'TX.concat'ing, at least with -O2 which you really need to be using 266 | -- with 'TX.Text' anyway. 267 | toReverseText :: YiString -> TX.Text 268 | toReverseText = TX.reverse . toText 269 | 270 | -- | Checks if the given 'YiString' is actually empty. 271 | null :: YiString -> Bool 272 | null = T.null . fromRope 273 | 274 | -- | Creates an empty 'YiString'. 275 | empty :: YiString 276 | empty = YiString T.empty 277 | 278 | -- | Length of the whole underlying string. 279 | -- 280 | -- Amortized constant time. 281 | length :: YiString -> Int 282 | length = charIndex . measure . fromRope 283 | 284 | -- | Count the number of newlines in the underlying string. This is 285 | -- actually amortized constant time as we cache this information in 286 | -- the underlying tree. 287 | countNewLines :: YiString -> Int 288 | countNewLines = lineIndex . measure . fromRope 289 | 290 | -- | Append two 'YiString's. 291 | -- 292 | -- We take the extra time to optimise this append for many small 293 | -- insertions. With naive append of the inner fingertree with 'T.><', 294 | -- it is often the case that we end up with a large collection of tiny 295 | -- chunks. This function instead tries to join the underlying trees at 296 | -- outermost chunks up to 'defaultChunkSize' which while slower, 297 | -- should improve memory usage. 298 | -- 299 | -- I suspect that this pays for itself as we'd spend more time 300 | -- computing over all the little chunks than few large ones anyway. 301 | append :: YiString -> YiString -> YiString 302 | append (YiString t) (YiString t') = case (viewr t, viewl t') of 303 | (EmptyR, _) -> YiString t' 304 | (_, EmptyL) -> YiString t 305 | (ts :> Chunk l x, Chunk l' x' :< ts') -> 306 | let len = l + l' in case compare len defaultChunkSize of 307 | GT -> YiString (t <> t') 308 | _ -> YiString (ts |- Chunk len (x <> x') <> ts') 309 | 310 | -- | Concat a list of 'YiString's. 311 | concat :: [YiString] -> YiString 312 | concat = L.foldl' append empty 313 | 314 | -- | Take the first character of the underlying string if possible. 315 | head :: YiString -> Maybe Char 316 | head (YiString t) = case viewl t of 317 | EmptyL -> Nothing 318 | Chunk _ x :< _ -> if TX.null x then Nothing else Just (TX.head x) 319 | 320 | -- | Take the last character of the underlying string if possible. 321 | last :: YiString -> Maybe Char 322 | last (YiString t) = case viewr t of 323 | EmptyR -> Nothing 324 | _ :> Chunk _ x -> if TX.null x then Nothing else Just (TX.last x) 325 | 326 | -- | Takes every character but the last one: returns Nothing on empty 327 | -- string. 328 | init :: YiString -> Maybe YiString 329 | init (YiString t) = case viewr t of 330 | EmptyR -> Nothing 331 | ts :> Chunk 0 _ -> Yi.Rope.init (YiString ts) 332 | ts :> Chunk l x -> Just . YiString $ ts |- Chunk (l - 1) (TX.init x) 333 | 334 | -- | Takes the tail of the underlying string. If the string is empty 335 | -- to begin with, returns Nothing. 336 | tail :: YiString -> Maybe YiString 337 | tail (YiString t) = case viewl t of 338 | EmptyL -> Nothing 339 | Chunk 0 _ :< ts -> Yi.Rope.tail (YiString ts) 340 | Chunk l x :< ts -> Just . YiString $ Chunk (l - 1) (TX.tail x) -| ts 341 | 342 | -- | Splits the string at given character position. 343 | -- 344 | -- If @position <= 0@ then the left string is empty and the right string 345 | -- contains everything else. 346 | -- 347 | -- If @position >= length of the string@ then the left string contains 348 | -- everything and the right string is empty. 349 | -- 350 | -- Implementation note: the way this works is by splitting the 351 | -- underlying finger at a closest chunk that goes *over* the given 352 | -- position (see 'T.split'). This either results in a perfect split at 353 | -- which point we're done or more commonly, it leaves as few 354 | -- characters short and we need to take few characters from the first 355 | -- chunk of the right side of the split. We do precisely that. 356 | -- 357 | -- All together, this split is only as expensive as underlying 358 | -- 'T.split', the cost of splitting a chunk into two, the cost of one 359 | -- cons and one cons of a chunk and lastly the cost of 'T.splitAt' of 360 | -- the underlying 'TX.Text'. It turns out to be fairly fast all 361 | -- together. 362 | splitAt :: Int -> YiString -> (YiString, YiString) 363 | splitAt n (YiString t) 364 | | n <= 0 = (mempty, YiString t) 365 | | otherwise = case viewl s of 366 | Chunk l x :< ts | n' /= 0 -> 367 | let (lx, rx) = TX.splitAt n' x 368 | in (YiString $ f |> Chunk n' lx, 369 | YiString $ Chunk (l - n') rx -| ts) 370 | _ -> (YiString f, YiString s) 371 | where 372 | (f, s) = T.split ((> n) . charIndex) t 373 | n' = n - charIndex (measure f) 374 | 375 | -- | Takes the first n given characters. 376 | take :: Int -> YiString -> YiString 377 | take 1 = maybe mempty Yi.Rope.singleton . Yi.Rope.head 378 | take n = fst . Yi.Rope.splitAt n 379 | 380 | -- | Drops the first n characters. 381 | drop :: Int -> YiString -> YiString 382 | drop 1 = fromMaybe mempty . Yi.Rope.tail 383 | drop n = snd . Yi.Rope.splitAt n 384 | 385 | -- | The usual 'Prelude.dropWhile' optimised for 'YiString's. 386 | dropWhile :: (Char -> Bool) -> YiString -> YiString 387 | dropWhile p = YiString . go . fromRope 388 | where 389 | go t = case viewl t of 390 | EmptyL -> T.empty 391 | Chunk 0 _ :< ts -> go ts 392 | Chunk l x :< ts -> 393 | let r = TX.dropWhile p x 394 | l' = TX.length r 395 | in case compare l' l of 396 | -- We dropped nothing so we must be done. 397 | EQ -> t 398 | -- We dropped something, if it was everything then drop from 399 | -- next chunk. 400 | LT | TX.null r -> go ts 401 | -- It wasn't everything and we have left-overs, we must be done. 402 | | otherwise -> Chunk l' r <| ts 403 | -- We shouldn't really get here or it would mean that 404 | -- dropping stuff resulted in more content than we had. This 405 | -- can only happen if unsafe functions don't preserve the 406 | -- chunk size and it goes out of sync with the text length. 407 | -- Preserve this abomination, it may be useful for 408 | -- debugging. 409 | _ -> Chunk l' r -| ts 410 | 411 | -- | As 'Yi.Rope.dropWhile' but drops from the end instead. 412 | dropWhileEnd :: (Char -> Bool) -> YiString -> YiString 413 | dropWhileEnd p = YiString . go . fromRope 414 | where 415 | go t = case viewr t of 416 | EmptyR -> T.empty 417 | ts :> Chunk 0 _ -> go ts 418 | ts :> Chunk l x -> 419 | let r = TX.dropWhileEnd p x 420 | l' = TX.length r 421 | in case compare l' l of 422 | EQ -> t 423 | LT | TX.null r -> go ts 424 | | otherwise -> ts |> Chunk l' r 425 | _ -> ts |- Chunk l' r 426 | 427 | -- | The usual 'Prelude.takeWhile' optimised for 'YiString's. 428 | takeWhile :: (Char -> Bool) -> YiString -> YiString 429 | takeWhile p = YiString . go . fromRope 430 | where 431 | go t = case viewl t of 432 | EmptyL -> T.empty 433 | Chunk 0 _ :< ts -> go ts 434 | Chunk l x :< ts -> 435 | let r = TX.takeWhile p x 436 | l' = TX.length r 437 | in case compare l' l of 438 | -- We took the whole chunk, keep taking more. 439 | EQ -> Chunk l x -| go ts 440 | -- We took some stuff but not everything so we're done. 441 | -- Alternatively, we took more than the size chunk so 442 | -- preserve this wonder. This should only ever happen if you 443 | -- use unsafe functions and Chunk size goes out of sync with 444 | -- actual text length. 445 | _ -> T.singleton $ Chunk l' r 446 | 447 | -- | Like 'Yi.Rope.takeWhile' but takes from the end instead. 448 | takeWhileEnd :: (Char -> Bool) -> YiString -> YiString 449 | takeWhileEnd p = YiString . go . fromRope 450 | where 451 | go t = case viewr t of 452 | EmptyR -> T.empty 453 | ts :> Chunk 0 _ -> go ts 454 | ts :> Chunk l x -> case compare l' l of 455 | EQ -> go ts |> Chunk l x 456 | _ -> T.singleton $ Chunk l' r 457 | where 458 | -- no TX.takeWhileEnd – https://github.com/bos/text/issues/89 459 | r = TX.reverse . TX.takeWhile p . TX.reverse $ x 460 | l' = TX.length r 461 | 462 | 463 | -- | Returns a pair whose first element is the longest prefix 464 | -- (possibly empty) of t of elements that satisfy p, and whose second 465 | -- is the remainder of the string. See also 'TX.span'. 466 | -- 467 | -- This implementation uses 'Yi.Rope.splitAt' which actually is just 468 | -- as fast as hand-unrolling the tree. GHC sure is great! 469 | span :: (Char -> Bool) -> YiString -> (YiString, YiString) 470 | span p y = let x = Yi.Rope.takeWhile p y 471 | in case Yi.Rope.splitAt (Yi.Rope.length x) y of 472 | -- Re-using ‘x’ seems to gain us a minor performance 473 | -- boost. 474 | (_, y') -> (x, y') 475 | 476 | -- | Just like 'Yi.Rope.span' but with the predicate negated. 477 | break :: (Char -> Bool) -> YiString -> (YiString, YiString) 478 | break p = Yi.Rope.span (not . p) 479 | 480 | -- | Concatenates the list of 'YiString's after inserting the 481 | -- user-provided 'YiString' between the elements. 482 | -- 483 | -- Empty 'YiString's are not ignored and will end up as strings of 484 | -- length 1. If you don't want this, it's up to you to pre-process the 485 | -- list. Just as with 'Yi.Rope.intersperse', it is up to the user to 486 | -- pre-process the list. 487 | intercalate :: YiString -> [YiString] -> YiString 488 | intercalate _ [] = mempty 489 | intercalate (YiString t') (YiString s:ss) = YiString $ go s ss 490 | where 491 | go !acc [] = acc 492 | go acc (YiString t : ts') = go (acc >< t' >< t) ts' 493 | 494 | -- | Intersperses the given character between the 'YiString's. This is 495 | -- useful when you have a bunch of strings you just want to separate 496 | -- something with, comma or a dash. Note that it only inserts the 497 | -- character between the elements. 498 | -- 499 | -- What's more, the result is a single 'YiString'. You can easily 500 | -- achieve a version that blindly inserts elements to the back by 501 | -- mapping over the list instead of using this function. 502 | -- 503 | -- You can think of it as a specialised version of 504 | -- 'Yi.Rope.intercalate'. Note that what this does __not__ do is 505 | -- intersperse characters into the underlying text, you should convert 506 | -- and use 'TX.intersperse' for that instead. 507 | intersperse :: Char -> [YiString] -> YiString 508 | intersperse _ [] = mempty 509 | intersperse c (t:ts) = go t ts 510 | where 511 | go !acc [] = acc 512 | go acc (t':ts') = go (acc <> (c `cons` t')) ts' 513 | 514 | -- | Add a 'Char' in front of a 'YiString'. 515 | cons :: Char -> YiString -> YiString 516 | cons c (YiString t) = case viewl t of 517 | EmptyL -> Yi.Rope.singleton c 518 | Chunk l x :< ts | l < defaultChunkSize -> YiString $ Chunk (l + 1) (c `TX.cons` x) <| ts 519 | _ -> YiString $ Chunk 1 (TX.singleton c) <| t 520 | 521 | -- | Add a 'Char' in the back of a 'YiString'. 522 | snoc :: YiString -> Char -> YiString 523 | snoc (YiString t) c = case viewr t of 524 | EmptyR -> Yi.Rope.singleton c 525 | ts :> Chunk l x | l < defaultChunkSize -> YiString $ ts |> Chunk (l + 1) (x `TX.snoc` c) 526 | _ -> YiString $ t |> Chunk 1 (TX.singleton c) 527 | 528 | -- | Single character 'YiString'. Consider whether it's worth creating 529 | -- this, maybe you can use 'cons' or 'snoc' instead? 530 | singleton :: Char -> YiString 531 | singleton c = YiString . T.singleton $ Chunk 1 (TX.singleton c) 532 | 533 | -- | Splits the underlying string before the given line number. 534 | -- Zero-indexed lines. 535 | -- 536 | -- Splitting at line <= 0 gives you an empty string. Splitting at 537 | -- @n > 0@ gives you the first n lines. 538 | -- 539 | -- Also see 'splitAtLine''. 540 | splitAtLine :: Int -> YiString -> (YiString, YiString) 541 | splitAtLine n r | n <= 0 = (empty, r) 542 | | otherwise = splitAtLine' (n - 1) r 543 | 544 | -- | Splits the underlying string after the given line number. 545 | -- Zero-indexed lines. 546 | -- 547 | -- Splitting at line <= 0 gives you the first line. Splitting at 548 | -- @n > 0@ gives you the first n + 1 lines. 549 | -- 550 | -- The implementation is similar to that of 'splitAt' except we are 551 | -- now looking for extra newlines in the next chunk rather than extra 552 | -- characters. 553 | splitAtLine' :: Int -> YiString -> (YiString, YiString) 554 | splitAtLine' p (YiString tr) = case viewl s of 555 | ch@(Chunk _ x) :< r -> 556 | let excess = lineIndex (measure f) + lineIndex (measure ch) - p - 1 557 | (lx, rx) = cutExcess excess x 558 | in (YiString $ f |- mkChunk TX.length lx, 559 | YiString $ mkChunk TX.length rx -| r) 560 | _ -> (YiString f, YiString s) 561 | where 562 | (f, s) = T.split ((p <) . lineIndex) tr 563 | 564 | cutExcess :: Int -> TX.Text -> (TX.Text, TX.Text) 565 | cutExcess n t = case TX.length t of 566 | 0 -> (TX.empty, TX.empty) 567 | _ -> let ns = countNl t 568 | ls = TX.lines t 569 | front = TX.unlines $ Prelude.take (ns - n) ls 570 | back = TX.drop (TX.length front) t 571 | in if n >= ns 572 | then (t, TX.empty) 573 | else (front, back) 574 | 575 | -- | This is like 'lines'' but it does *not* preserve newlines. 576 | -- 577 | -- Specifically, we just strip the newlines from the result of 578 | -- 'lines''. 579 | -- 580 | -- This behaves slightly differently than the old split: the number of 581 | -- resulting strings here is equal to the number of newline characters 582 | -- in the underlying string. This is much more consistent than the old 583 | -- behaviour which blindly used @ByteString@s split and stitched the 584 | -- result back together which was inconsistent with the rest of the 585 | -- interface which worked with number of newlines. 586 | lines :: YiString -> [YiString] 587 | lines = Prelude.map dropNl . lines' 588 | where 589 | dropNl (YiString t) = case viewr t of 590 | EmptyR -> Yi.Rope.empty 591 | ts :> ch@(Chunk l tx) -> 592 | YiString $ ts |- if TX.null tx 593 | then ch 594 | else case TX.last tx of 595 | '\n' -> Chunk (l - 1) (TX.init tx) 596 | _ -> ch 597 | 598 | -- | Splits the 'YiString' into a list of 'YiString' each containing a 599 | -- line. 600 | -- 601 | -- Note that in old implementation this allowed an arbitrary character 602 | -- to split on. If you want to do that, manually convert 'toText' and 603 | -- use 'TX.splitOn' to suit your needs. This case is optimised for 604 | -- newlines only which seems to have been the only use of the original 605 | -- function. 606 | -- 607 | -- The newlines are preserved so this should hold: 608 | -- 609 | -- > 'toText' . 'concat' . 'lines'' ≡ 'toText' 610 | -- 611 | -- but the underlying structure might change: notably, chunks will 612 | -- most likely change sizes. 613 | lines' :: YiString -> [YiString] 614 | lines' t = let (YiString f, YiString s) = splitAtLine' 0 t 615 | in if T.null s 616 | then if T.null f then [] else [YiString f] 617 | else YiString f : lines' (YiString s) 618 | 619 | -- | Joins up lines by a newline character. It does not leave a 620 | -- newline after the last line. If you want a more classical 621 | -- 'Prelude.unlines' behaviour, use 'Yi.Rope.map' along with 622 | -- 'Yi.Rope.snoc'. 623 | unlines :: [YiString] -> YiString 624 | unlines = Yi.Rope.intersperse '\n' 625 | 626 | -- | 'YiString' specialised @any@. 627 | -- 628 | -- Implementation note: this currently just does any by doing ‘TX.Text’ 629 | -- conversions upon consecutive chunks. We should be able to speed it 630 | -- up by running it in parallel over multiple chunks. 631 | any :: (Char -> Bool) -> YiString -> Bool 632 | any p = go . fromRope 633 | where 634 | go x = case viewl x of 635 | EmptyL -> False 636 | Chunk _ t :< ts -> TX.any p t || go ts 637 | 638 | -- | 'YiString' specialised @all@. 639 | -- 640 | -- See the implementation note for 'Yi.Rope.any'. 641 | all :: (Char -> Bool) -> YiString -> Bool 642 | all p = go . fromRope 643 | where 644 | go x = case viewl x of 645 | EmptyL -> True 646 | Chunk _ t :< ts -> TX.all p t && go ts 647 | 648 | -- | To serialise a 'YiString', we turn it into a regular 'String' 649 | -- first. 650 | instance Binary YiString where 651 | put = put . toString 652 | get = Yi.Rope.fromString <$> get 653 | 654 | -- | Write a 'YiString' into the given file. 655 | -- 656 | -- It's up to the user to handle exceptions. 657 | writeFile :: FilePath -> YiString -> IO () 658 | writeFile f = TXIO.writeFile f . toText 659 | 660 | -- | Reads file into the rope, also returning the 'ConverterName' that 661 | -- was used for decoding. You should resupply this to 'writeFile' if 662 | -- you're aiming to preserve the original encoding. 663 | -- 664 | -- If we fail to guess the encoding used, error message is given 665 | -- instead. 666 | -- 667 | -- It is up to the user to handle exceptions not directly related to 668 | -- character decoding. 669 | readFile :: FilePath -> IO (Either TX.Text YiString) 670 | readFile fp = BSL.readFile fp >>= go decoders 671 | where 672 | go [] _ = pure (Left err) 673 | go (d : ds) bytes = 674 | try (pure (d bytes)) >>= \case 675 | Left (_ :: TXEE.UnicodeException) -> go ds bytes 676 | Right text -> pure (Right (fromLazyText text)) 677 | err = "Could not guess the encoding of " <> TX.pack fp 678 | decoders = 679 | [ TXLE.decodeUtf8 680 | , TXLE.decodeUtf16LE 681 | , TXLE.decodeUtf16BE 682 | , TXLE.decodeUtf32LE 683 | , TXLE.decodeUtf32BE 684 | ] 685 | 686 | -- | Filters the characters from the underlying string. 687 | -- 688 | -- >>> filter (/= 'a') "bac" 689 | -- "bc" 690 | filter :: (Char -> Bool) -> YiString -> YiString 691 | filter p = YiString . go . fromRope 692 | where 693 | go t = case viewl t of 694 | EmptyL -> T.empty 695 | Chunk _ x :< ts -> mkChunk TX.length (TX.filter p x) -| go ts 696 | 697 | -- | Maps the characters over the underlying string. 698 | map :: (Char -> Char) -> YiString -> YiString 699 | map f = YiString . go . fromRope 700 | where 701 | go t = case viewl t of 702 | EmptyL -> T.empty 703 | Chunk l x :< ts -> Chunk l (TX.map f x) <| go ts 704 | 705 | -- | Join given 'YiString's with a space. Empty lines will be filtered 706 | -- out first. 707 | unwords :: [YiString] -> YiString 708 | unwords = Yi.Rope.intersperse ' ' 709 | 710 | -- | Splits the given 'YiString' into a list of words, where spaces 711 | -- are determined by 'isSpace'. No empty strings are in the result 712 | -- list. 713 | words :: YiString -> [YiString] 714 | words = Prelude.filter (not . Yi.Rope.null) . Yi.Rope.split isSpace 715 | 716 | -- | Splits the 'YiString' on characters matching the predicate, like 717 | -- 'TX.split'. 718 | -- 719 | -- For splitting on newlines use 'Yi.Rope.lines' or 'Yi.Rope.lines'' 720 | -- instead. 721 | -- 722 | -- Implementation note: GHC actually makes this naive implementation 723 | -- about as fast and in cases with lots of splits, faster, as a 724 | -- hand-rolled version on chunks with appends which is quite amazing 725 | -- in itself. 726 | split :: (Char -> Bool) -> YiString -> [YiString] 727 | split p = fmap fromText . TX.split p . toText 728 | 729 | -- | Left fold. 730 | -- 731 | -- Benchmarks show that folding is actually Pretty Damn Slow™: consider 732 | -- whether folding is really the best thing to use in your scenario. 733 | foldl' :: (a -> Char -> a) -> a -> YiString -> a 734 | foldl' f a = go a . fromRope 735 | where 736 | go acc t = case viewl t of 737 | EmptyL -> acc 738 | Chunk _ x :< ts -> let r = TX.foldl' f acc x 739 | in r `seq` go r ts 740 | 741 | -- | Replicate the given YiString set number of times, concatenating 742 | -- the results. Also see 'Yi.Rope.replicateChar'. 743 | replicate :: Int -> YiString -> YiString 744 | replicate n t | n <= 0 = mempty 745 | | otherwise = t <> Yi.Rope.replicate (n - 1) t 746 | 747 | -- | Replicate the given character set number of times and pack the 748 | -- result into a 'YiString'. 749 | -- 750 | -- >>> replicateChar 4 ' ' 751 | -- " " 752 | replicateChar :: Int -> Char -> YiString 753 | replicateChar n = fromText . TX.replicate n . TX.singleton 754 | 755 | -- | Helper function doing conversions of to and from underlying 756 | -- 'TX.Text'. You should aim to implement everything in terms of 757 | -- 'YiString' instead. 758 | -- 759 | -- Please note that this maps over each __chunk__ so this can only be 760 | -- used with layout-agnostic functions. For example 761 | -- 762 | -- >>> let t = 'fromString' "abc" <> 'fromString' "def" 763 | -- >>> 'toString' $ 'withText' 'TX.reverse' t 764 | -- "cbafed" 765 | -- 766 | -- Probably doesn't do what you wanted, but 'TX.toUpper' would. 767 | -- Specifically, for any @f : 'TX.Text' → 'TX.Text'@, 'withText' will 768 | -- only do the ‘expected’ thing iff 769 | -- 770 | -- @f x <> f y ≡ f (x <> y)@ 771 | -- 772 | -- which should look very familiar. 773 | withText :: (TX.Text -> TX.Text) -> YiString -> YiString 774 | withText f = YiString . T.fmap' (mkChunk TX.length . f . _fromChunk) . fromRope 775 | 776 | -- | Maps over each __chunk__ which means this function is UNSAFE! If 777 | -- you use this with functions which don't preserve 'Size', that is 778 | -- the chunk length and number of newlines, things will break really, 779 | -- really badly. You should not need to use this. 780 | -- 781 | -- Also see 'T.unsafeFmap' 782 | unsafeWithText :: (TX.Text -> TX.Text) -> YiString -> YiString 783 | unsafeWithText f = YiString . T.unsafeFmap g . fromRope 784 | where 785 | g (Chunk l t) = Chunk l (f t) 786 | -------------------------------------------------------------------------------- /test/Spec.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -F -pgmF hspec-discover #-} 2 | -------------------------------------------------------------------------------- /test/Yi/RopeSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module Yi.RopeSpec (main, spec) where 3 | 4 | import Data.Char (isUpper, toUpper, isSpace) 5 | import qualified Data.Text as T 6 | import Test.Hspec 7 | import Test.Hspec.QuickCheck 8 | import Test.QuickCheck.Instances () 9 | import qualified Yi.Rope as R 10 | 11 | main :: IO () 12 | main = hspec spec 13 | 14 | infix 1 `isLike` 15 | -- | Converts the input to R.YiString before comparing results. 16 | isLike :: (Show a, Eq a) => (R.YiString -> a) 17 | -> (T.Text -> a) 18 | -> T.Text 19 | -> Expectation 20 | f `isLike` g = \t -> (f . R.fromText) t `shouldBe` g t 21 | 22 | infix 1 `isLikeT` 23 | -- | Applies given function over underlying 'R.YiString'. 24 | isLikeT :: (R.YiString -> R.YiString) 25 | -> (T.Text -> T.Text) 26 | -> T.Text 27 | -> Expectation 28 | f `isLikeT` g = \t -> (R.toText . f . R.fromText) t `shouldBe` g t 29 | 30 | spec :: Spec 31 | spec = modifyMaxSize (const 1000) $ do 32 | describe "Working with YiString is just like working with Text" $ do 33 | prop "id ~ id" $ id `isLikeT` id 34 | prop "R.take ~ T.take" $ \i -> R.take i `isLikeT` T.take i 35 | prop "R.drop ~ T.drop" $ \i -> R.drop i `isLikeT` T.drop i 36 | prop "R.reverse ~ T.reverse" $ R.reverse `isLikeT` T.reverse 37 | prop "R.length ~ T.length" $ R.length `isLike` T.length 38 | prop "R.null ~ T.null" $ R.null `isLike` T.null 39 | prop "R.countNewLines ~ T.count \\n" $ R.countNewLines `isLike` T.count "\n" 40 | prop "R.concat . R.lines' = id" $ (R.toText . R.concat . R.lines') `isLike` id 41 | prop "R.lines ~ T.lines" $ (fmap R.toText . R.lines) `isLike` T.lines 42 | prop "R.empty ~ T.empty" $ R.toText R.empty `shouldBe` T.empty 43 | prop "fst . R.splitAt ~ fst . T.splitAt" $ \i -> 44 | fst . R.splitAt i `isLikeT` fst . T.splitAt i 45 | prop "snd . R.splitAt ~ snd . T.splitAt" $ \i -> 46 | snd . R.splitAt i `isLikeT` snd . T.splitAt i 47 | prop "R.append ~ T.append" $ \t -> 48 | R.append (R.fromText t) `isLikeT` T.append t 49 | prop "R.concat ~ T.concat" $ \s -> 50 | (R.toText . R.concat . map R.fromText) s `shouldBe` T.concat s 51 | prop "R.head ~ T.head" $ R.head `isLike` (\x -> if T.null x 52 | then Nothing 53 | else Just (T.head x)) 54 | prop "R.last ~ T.last" $ R.last `isLike` (\x -> if T.null x 55 | then Nothing 56 | else Just (T.last x)) 57 | prop "R.cons ~ T.cons" $ \c -> R.cons c `isLikeT` T.cons c 58 | prop "R.snoc ~ T.snoc" $ \c -> R.cons c `isLikeT` T.cons c 59 | prop "R.singleton ~ T.singleton" $ 60 | \c -> (R.toText . R.singleton) c `shouldBe` T.singleton c 61 | prop "\\p -> R.any p ~ T.any p $ const True" $ \t -> 62 | R.any (const True) (R.fromText t) `shouldBe` T.any (const True) t 63 | prop "\\p -> R.any p ~ T.any p $ const False" $ \t -> 64 | R.any (const False) (R.fromText t) `shouldBe` T.any (const False) t 65 | prop "\\p c -> R.any (== c) p ~ T.any (== c) p" $ \c t -> 66 | R.any (== c) (R.fromText t) `shouldBe` T.any (== c) t 67 | prop "\\p c -> R.all (== c) p ~ T.all (== c) p" $ \c t -> 68 | R.all (== c) (R.fromText t) `shouldBe` T.all (== c) t 69 | prop "\\f -> R.withText ~ f $ T.toTitle" $ 70 | R.withText T.toTitle `isLikeT` T.toTitle 71 | prop "\\p -> R.dropWhile p ~ T.dropWhile p $ isUpper" $ 72 | R.dropWhile isUpper `isLikeT` T.dropWhile isUpper 73 | prop "\\p -> R.takeWhile p ~ T.takeWhile p $ isUpper" $ 74 | R.takeWhile isUpper `isLikeT` T.takeWhile isUpper 75 | prop "R.compare ~ T.compare" $ \t t' -> 76 | compare (R.fromText t) (R.fromText t') `shouldBe` compare t t' 77 | prop "\\p -> R.filter p ~ T.filter p $ isUpper" $ 78 | R.filter isUpper `isLikeT` T.filter isUpper 79 | prop "\\f -> R.map f ~ T.map f $ toUpper" $ 80 | R.map toUpper `isLikeT` T.map toUpper 81 | prop "\\p -> R.dropWhileEnd p ~ T.dropWhileEnd p $ isSpace" $ 82 | R.dropWhileEnd isSpace `isLikeT` T.dropWhileEnd isSpace 83 | prop "\\p -> R.takeWhileEnd p ~ rev . T.takeWhile p . rev $ isSpace" $ 84 | R.takeWhileEnd isSpace 85 | `isLikeT` T.reverse . T.takeWhile isSpace . T.reverse 86 | prop "R.words ~ T.words" $ \t -> 87 | R.toText <$> R.words (R.fromText t) `shouldBe` T.words t 88 | prop "R.unwords ~ T.unwords" $ \t -> 89 | R.toText (R.unwords (R.fromText <$> t)) `shouldBe` T.unwords t 90 | prop "\\p -> R.split p ~ T.split p $ isUpper" $ \t -> 91 | R.toText <$> R.split isUpper (R.fromText t) `shouldBe` T.split isUpper t 92 | prop "non-empty s ⊢ R.init s ~ T.init s" $ \t -> 93 | let t' = t `T.snoc` 'a' -- ensure non-empty 94 | in (fmap R.toText . R.init . R.fromText) t' `shouldBe` (Just . T.init) t' 95 | prop "non-empty s ⊢ R.tail s ~ T.tail s" $ \t -> 96 | let t' = t `T.snoc` 'a' 97 | in (fmap R.toText . R.tail . R.fromText) t' `shouldBe` (Just . T.tail) t' 98 | prop "\\p -> R.span p ~ T.span p $ isUpper" $ \t -> 99 | let (f, s) `shouldBeT` y = (R.toText f, R.toText s) `shouldBe` y 100 | in (R.span isUpper . R.fromText) t `shouldBeT` T.span isUpper t 101 | prop "\\p -> R.break p ~ T.break p $ isUpper" $ \t -> 102 | let (f, s) `shouldBeT` y = (R.toText f, R.toText s) `shouldBe` y 103 | in (R.break isUpper . R.fromText) t `shouldBeT` T.break isUpper t 104 | prop "\\p -> R.foldl' p ~ T.foldl' p $ \\x _ -> x + 1" $ \t -> 105 | let f x _ = x + (1 :: Integer) 106 | in (R.foldl' f 0 . R.fromText) t `shouldBe` T.foldl' f 0 t 107 | prop "R.replicate ~ T.replicate" $ \n -> 108 | R.replicate n `isLikeT` T.replicate n 109 | prop "R.intercalate ~ T.intercalate" $ \t ts -> 110 | R.toText (R.intercalate (R.fromText t) (R.fromText <$> ts)) 111 | `shouldBe` T.intercalate t ts 112 | describe "But R.intersperse is not like T.intersperse" $ do 113 | prop "R.intercalate (R.singleton c) = R.intersperse c" $ \c ts -> 114 | let rs = R.fromText <$> ts 115 | in R.intercalate (R.singleton c) rs `shouldBe` R.intersperse c rs -------------------------------------------------------------------------------- /yi-rope.cabal: -------------------------------------------------------------------------------- 1 | name: yi-rope 2 | version: 0.11 3 | synopsis: A rope data structure used by Yi 4 | description: A rope data structure used by Yi 5 | license: GPL-2 6 | license-file: LICENSE 7 | author: AUTHORS 8 | maintainer: yi-devel@googlegroups.com 9 | category: Yi 10 | build-type: Simple 11 | cabal-version: >=1.10 12 | 13 | library 14 | ghc-options: -fpedantic-bottoms -fexpose-all-unfoldings -ferror-spans -Wall -O2 -flate-dmd-anal 15 | 16 | exposed-modules: 17 | Yi.Rope 18 | 19 | build-depends: 20 | base >=4.8 && <5 21 | , binary 22 | , bytestring >= 0.10 23 | , deepseq 24 | , fingertree >= 0.1.1 25 | , text >= 1.2 26 | 27 | hs-source-dirs: src 28 | default-language: Haskell2010 29 | 30 | test-suite spec 31 | type: exitcode-stdio-1.0 32 | default-language: Haskell2010 33 | main-is: Spec.hs 34 | hs-source-dirs: test 35 | ghc-options: -Wall -O2 -rtsopts 36 | other-modules: 37 | Yi.RopeSpec 38 | 39 | build-depends: 40 | base 41 | , hspec 42 | , QuickCheck == 2.* 43 | , quickcheck-instances 44 | , text 45 | , yi-rope 46 | 47 | benchmark bench 48 | type: exitcode-stdio-1.0 49 | default-language: Haskell2010 50 | main-is: MainBenchmarkSuite.hs 51 | hs-source-dirs: bench 52 | ghc-options: -Wall -O2 53 | 54 | build-depends: 55 | base >=4.8 && <5 56 | , criterion 57 | , deepseq 58 | , text 59 | , yi-rope 60 | 61 | source-repository head 62 | type: git 63 | location: https://github.com/yi-editor/yi-rope.git 64 | --------------------------------------------------------------------------------