├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── m ├── bindings.rs ├── buffer.rs ├── command.rs ├── editor.rs ├── input.rs ├── iterators.rs ├── key.rs ├── lib.rs ├── log.rs ├── textobject.rs ├── utils.rs └── view.rs └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | vendor/ 3 | .idea/ 4 | target/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - nightly 4 | os: 5 | - linux 6 | - osx 7 | script: 8 | - cargo build --verbose 9 | - cargo test --verbose 10 | notifications: 11 | webhooks: 12 | urls: 13 | - https://webhooks.gitter.im/e/79294a7e7b016f660d66 14 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "aho-corasick" 5 | version = "0.5.3" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | dependencies = [ 8 | "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", 9 | ] 10 | 11 | [[package]] 12 | name = "argon2rs" 13 | version = "0.2.5" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | dependencies = [ 16 | "blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", 17 | "scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", 18 | ] 19 | 20 | [[package]] 21 | name = "arrayvec" 22 | version = "0.4.10" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | dependencies = [ 25 | "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", 26 | ] 27 | 28 | [[package]] 29 | name = "autocfg" 30 | version = "0.1.4" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | 33 | [[package]] 34 | name = "backtrace" 35 | version = "0.3.32" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | dependencies = [ 38 | "backtrace-sys 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)", 39 | "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", 40 | "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 41 | "rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", 42 | ] 43 | 44 | [[package]] 45 | name = "backtrace-sys" 46 | version = "0.1.30" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | dependencies = [ 49 | "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)", 50 | "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 51 | ] 52 | 53 | [[package]] 54 | name = "bitflags" 55 | version = "0.2.1" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | 58 | [[package]] 59 | name = "bitflags" 60 | version = "1.1.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | 63 | [[package]] 64 | name = "blake2-rfc" 65 | version = "0.2.18" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | dependencies = [ 68 | "arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", 69 | "constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", 70 | ] 71 | 72 | [[package]] 73 | name = "block" 74 | version = "0.1.6" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | 77 | [[package]] 78 | name = "byteorder" 79 | version = "1.3.2" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | 82 | [[package]] 83 | name = "c2-chacha" 84 | version = "0.2.2" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | dependencies = [ 87 | "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 88 | "ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", 89 | ] 90 | 91 | [[package]] 92 | name = "cc" 93 | version = "1.0.37" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | 96 | [[package]] 97 | name = "cfg-if" 98 | version = "0.1.9" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | 101 | [[package]] 102 | name = "clipboard" 103 | version = "0.4.6" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | dependencies = [ 106 | "clipboard-win 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 107 | "objc 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", 108 | "objc-foundation 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 109 | "objc_id 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 110 | "x11-clipboard 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 111 | ] 112 | 113 | [[package]] 114 | name = "clipboard-win" 115 | version = "2.2.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | dependencies = [ 118 | "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 119 | ] 120 | 121 | [[package]] 122 | name = "clippy" 123 | version = "0.0.302" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | dependencies = [ 126 | "term 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", 127 | ] 128 | 129 | [[package]] 130 | name = "cloudabi" 131 | version = "0.0.3" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | dependencies = [ 134 | "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 135 | ] 136 | 137 | [[package]] 138 | name = "constant_time_eq" 139 | version = "0.1.3" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | 142 | [[package]] 143 | name = "dirs" 144 | version = "1.0.5" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | dependencies = [ 147 | "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 148 | "redox_users 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 149 | "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 150 | ] 151 | 152 | [[package]] 153 | name = "error-chain" 154 | version = "0.11.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | dependencies = [ 157 | "backtrace 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)", 158 | ] 159 | 160 | [[package]] 161 | name = "failure" 162 | version = "0.1.5" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | dependencies = [ 165 | "backtrace 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)", 166 | "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 167 | ] 168 | 169 | [[package]] 170 | name = "failure_derive" 171 | version = "0.1.5" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | dependencies = [ 174 | "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", 175 | "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", 176 | "syn 0.15.39 (registry+https://github.com/rust-lang/crates.io-index)", 177 | "synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", 178 | ] 179 | 180 | [[package]] 181 | name = "fuchsia-cprng" 182 | version = "0.1.1" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | 185 | [[package]] 186 | name = "gag" 187 | version = "0.1.10" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | dependencies = [ 190 | "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 191 | "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 192 | ] 193 | 194 | [[package]] 195 | name = "gapbuffer" 196 | version = "0.1.1" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | 199 | [[package]] 200 | name = "getrandom" 201 | version = "0.1.6" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | dependencies = [ 204 | "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 205 | "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 206 | ] 207 | 208 | [[package]] 209 | name = "kernel32-sys" 210 | version = "0.2.2" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | dependencies = [ 213 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 214 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 215 | ] 216 | 217 | [[package]] 218 | name = "lazy_static" 219 | version = "0.2.11" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | 222 | [[package]] 223 | name = "lazy_static" 224 | version = "1.3.0" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | dependencies = [ 227 | "spin 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", 228 | ] 229 | 230 | [[package]] 231 | name = "libc" 232 | version = "0.2.58" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | 235 | [[package]] 236 | name = "log" 237 | version = "0.4.6" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | dependencies = [ 240 | "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", 241 | ] 242 | 243 | [[package]] 244 | name = "m-editor" 245 | version = "0.1.0" 246 | dependencies = [ 247 | "clipboard 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", 248 | "clippy 0.0.302 (registry+https://github.com/rust-lang/crates.io-index)", 249 | "gapbuffer 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 250 | "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 251 | "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)", 252 | "rustbox 0.11.0 (git+https://github.com/gchp/rustbox)", 253 | "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 254 | ] 255 | 256 | [[package]] 257 | name = "malloc_buf" 258 | version = "0.0.6" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | dependencies = [ 261 | "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 262 | ] 263 | 264 | [[package]] 265 | name = "memchr" 266 | version = "0.1.11" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | dependencies = [ 269 | "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 270 | ] 271 | 272 | [[package]] 273 | name = "nodrop" 274 | version = "0.1.13" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | 277 | [[package]] 278 | name = "num-traits" 279 | version = "0.1.43" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | dependencies = [ 282 | "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 283 | ] 284 | 285 | [[package]] 286 | name = "num-traits" 287 | version = "0.2.8" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | dependencies = [ 290 | "autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 291 | ] 292 | 293 | [[package]] 294 | name = "objc" 295 | version = "0.2.6" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | dependencies = [ 298 | "malloc_buf 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", 299 | ] 300 | 301 | [[package]] 302 | name = "objc-foundation" 303 | version = "0.1.1" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | dependencies = [ 306 | "block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 307 | "objc 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", 308 | "objc_id 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 309 | ] 310 | 311 | [[package]] 312 | name = "objc_id" 313 | version = "0.1.1" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | dependencies = [ 316 | "objc 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", 317 | ] 318 | 319 | [[package]] 320 | name = "ppv-lite86" 321 | version = "0.2.5" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | 324 | [[package]] 325 | name = "proc-macro2" 326 | version = "0.4.30" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | dependencies = [ 329 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 330 | ] 331 | 332 | [[package]] 333 | name = "quote" 334 | version = "0.6.12" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | dependencies = [ 337 | "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", 338 | ] 339 | 340 | [[package]] 341 | name = "rand" 342 | version = "0.7.0" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | dependencies = [ 345 | "getrandom 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 346 | "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 347 | "rand_chacha 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 348 | "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", 349 | "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 350 | ] 351 | 352 | [[package]] 353 | name = "rand_chacha" 354 | version = "0.2.0" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | dependencies = [ 357 | "autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 358 | "c2-chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 359 | "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", 360 | ] 361 | 362 | [[package]] 363 | name = "rand_core" 364 | version = "0.3.1" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | dependencies = [ 367 | "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 368 | ] 369 | 370 | [[package]] 371 | name = "rand_core" 372 | version = "0.4.0" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | 375 | [[package]] 376 | name = "rand_core" 377 | version = "0.5.0" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | dependencies = [ 380 | "getrandom 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 381 | ] 382 | 383 | [[package]] 384 | name = "rand_hc" 385 | version = "0.2.0" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | dependencies = [ 388 | "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", 389 | ] 390 | 391 | [[package]] 392 | name = "rand_os" 393 | version = "0.1.3" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | dependencies = [ 396 | "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 397 | "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 398 | "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 399 | "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 400 | "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 401 | "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 402 | ] 403 | 404 | [[package]] 405 | name = "rdrand" 406 | version = "0.4.0" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | dependencies = [ 409 | "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 410 | ] 411 | 412 | [[package]] 413 | name = "redox_syscall" 414 | version = "0.1.54" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | 417 | [[package]] 418 | name = "redox_users" 419 | version = "0.3.0" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | dependencies = [ 422 | "argon2rs 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", 423 | "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 424 | "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", 425 | "redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)", 426 | ] 427 | 428 | [[package]] 429 | name = "regex" 430 | version = "0.1.80" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | dependencies = [ 433 | "aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", 434 | "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", 435 | "regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", 436 | "thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", 437 | "utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", 438 | ] 439 | 440 | [[package]] 441 | name = "regex-syntax" 442 | version = "0.3.9" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | 445 | [[package]] 446 | name = "remove_dir_all" 447 | version = "0.5.2" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | dependencies = [ 450 | "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 451 | ] 452 | 453 | [[package]] 454 | name = "rustbox" 455 | version = "0.11.0" 456 | source = "git+https://github.com/gchp/rustbox#7d82dac093b2d4381d226ea2bb4f1e6ada006658" 457 | dependencies = [ 458 | "bitflags 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 459 | "gag 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 460 | "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", 461 | "termbox-sys 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 462 | ] 463 | 464 | [[package]] 465 | name = "rustc-demangle" 466 | version = "0.1.15" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | 469 | [[package]] 470 | name = "scoped_threadpool" 471 | version = "0.1.9" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | 474 | [[package]] 475 | name = "spin" 476 | version = "0.5.0" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | 479 | [[package]] 480 | name = "syn" 481 | version = "0.15.39" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | dependencies = [ 484 | "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", 485 | "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", 486 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 487 | ] 488 | 489 | [[package]] 490 | name = "synstructure" 491 | version = "0.10.2" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | dependencies = [ 494 | "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", 495 | "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", 496 | "syn 0.15.39 (registry+https://github.com/rust-lang/crates.io-index)", 497 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 498 | ] 499 | 500 | [[package]] 501 | name = "tempfile" 502 | version = "3.1.0" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | dependencies = [ 505 | "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", 506 | "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 507 | "rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", 508 | "redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)", 509 | "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", 510 | "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 511 | ] 512 | 513 | [[package]] 514 | name = "term" 515 | version = "0.5.2" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | dependencies = [ 518 | "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", 519 | "dirs 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", 520 | "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 521 | ] 522 | 523 | [[package]] 524 | name = "termbox-sys" 525 | version = "0.2.11" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | 528 | [[package]] 529 | name = "thread-id" 530 | version = "2.0.0" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | dependencies = [ 533 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 534 | "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 535 | ] 536 | 537 | [[package]] 538 | name = "thread_local" 539 | version = "0.2.7" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | dependencies = [ 542 | "thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 543 | ] 544 | 545 | [[package]] 546 | name = "unicode-width" 547 | version = "0.1.5" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | 550 | [[package]] 551 | name = "unicode-xid" 552 | version = "0.1.0" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | 555 | [[package]] 556 | name = "utf8-ranges" 557 | version = "0.1.3" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | 560 | [[package]] 561 | name = "winapi" 562 | version = "0.2.8" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | 565 | [[package]] 566 | name = "winapi" 567 | version = "0.3.7" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | dependencies = [ 570 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 571 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 572 | ] 573 | 574 | [[package]] 575 | name = "winapi-build" 576 | version = "0.1.1" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | 579 | [[package]] 580 | name = "winapi-i686-pc-windows-gnu" 581 | version = "0.4.0" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | 584 | [[package]] 585 | name = "winapi-x86_64-pc-windows-gnu" 586 | version = "0.4.0" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | 589 | [[package]] 590 | name = "x11-clipboard" 591 | version = "0.2.2" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | dependencies = [ 594 | "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", 595 | "xcb 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", 596 | ] 597 | 598 | [[package]] 599 | name = "xcb" 600 | version = "0.8.2" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | dependencies = [ 603 | "libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 604 | "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", 605 | ] 606 | 607 | [metadata] 608 | "checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66" 609 | "checksum argon2rs 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3f67b0b6a86dae6e67ff4ca2b6201396074996379fba2b92ff649126f37cb392" 610 | "checksum arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "92c7fb76bc8826a8b33b4ee5bb07a247a81e76764ab4d55e8f73e3a4d8808c71" 611 | "checksum autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "0e49efa51329a5fd37e7c79db4621af617cd4e3e5bc224939808d076077077bf" 612 | "checksum backtrace 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)" = "18b50f5258d1a9ad8396d2d345827875de4261b158124d4c819d9b351454fae5" 613 | "checksum backtrace-sys 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)" = "5b3a000b9c543553af61bc01cbfc403b04b5caa9e421033866f2e98061eb3e61" 614 | "checksum bitflags 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a41f80ec2e140d19e789764fdf22d0f2da98fe7e55d26f99db59cb3d2605d327" 615 | "checksum bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd" 616 | "checksum blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" 617 | "checksum block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" 618 | "checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" 619 | "checksum c2-chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7d64d04786e0f528460fc884753cf8dddcc466be308f6026f8e355c41a0e4101" 620 | "checksum cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "39f75544d7bbaf57560d2168f28fd649ff9c76153874db88bdbdfd839b1a7e7d" 621 | "checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" 622 | "checksum clipboard 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b9b4623b47d8637fc9d47564583d4cc01eb8c8e34e26b2bf348bf4b036acb657" 623 | "checksum clipboard-win 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b" 624 | "checksum clippy 0.0.302 (registry+https://github.com/rust-lang/crates.io-index)" = "d911ee15579a3f50880d8c1d59ef6e79f9533127a3bd342462f5d584f5e8c294" 625 | "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" 626 | "checksum constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8ff012e225ce166d4422e0e78419d901719760f62ae2b7969ca6b564d1b54a9e" 627 | "checksum dirs 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" 628 | "checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3" 629 | "checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" 630 | "checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" 631 | "checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 632 | "checksum gag 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8cc0b9f53275dc5fada808f1d2f82e3688a6c14d735633d1590b7be8eb2307b5" 633 | "checksum gapbuffer 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3e929b3ff01e4accdce7f5596a044890b5052ab7418ba8ce9ce5865d26ae4417" 634 | "checksum getrandom 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "e65cce4e5084b14874c4e7097f38cab54f47ee554f9194673456ea379dcc4c55" 635 | "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 636 | "checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" 637 | "checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" 638 | "checksum libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "6281b86796ba5e4366000be6e9e18bf35580adf9e63fbe2294aadb587613a319" 639 | "checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" 640 | "checksum malloc_buf 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 641 | "checksum memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20" 642 | "checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945" 643 | "checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" 644 | "checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" 645 | "checksum objc 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "31d20fd2b37e07cf5125be68357b588672e8cefe9a96f8c17a9d46053b3e590d" 646 | "checksum objc-foundation 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" 647 | "checksum objc_id 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" 648 | "checksum ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e3cbf9f658cdb5000fcf6f362b8ea2ba154b9f146a61c7a20d647034c6b6561b" 649 | "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" 650 | "checksum quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "faf4799c5d274f3868a4aae320a0a182cbd2baee377b378f080e16a23e9d80db" 651 | "checksum rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d47eab0e83d9693d40f825f86948aa16eff6750ead4bdffc4ab95b8b3a7f052c" 652 | "checksum rand_chacha 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e193067942ef6f485a349a113329140d0ab9e2168ce92274499bb0e9a4190d9d" 653 | "checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 654 | "checksum rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d0e7a549d590831370895ab7ba4ea0c1b6b011d106b5ff2da6eee112615e6dc0" 655 | "checksum rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "615e683324e75af5d43d8f7a39ffe3ee4a9dc42c5c701167a71dc59c3a493aca" 656 | "checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 657 | "checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" 658 | "checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 659 | "checksum redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)" = "12229c14a0f65c4f1cb046a3b52047cdd9da1f4b30f8a39c5063c8bae515e252" 660 | "checksum redox_users 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3fe5204c3a17e97dde73f285d49be585df59ed84b50a872baf416e73b62c3828" 661 | "checksum regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f" 662 | "checksum regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957" 663 | "checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" 664 | "checksum rustbox 0.11.0 (git+https://github.com/gchp/rustbox)" = "" 665 | "checksum rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "a7f4dccf6f4891ebcc0c39f9b6eb1a83b9bf5d747cb439ec6fba4f3b977038af" 666 | "checksum scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" 667 | "checksum spin 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "44363f6f51401c34e7be73db0db371c04705d35efbe9f7d6082e03a921a32c55" 668 | "checksum syn 0.15.39 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d960b829a55e56db167e861ddb43602c003c7be0bee1d345021703fac2fb7c" 669 | "checksum synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "02353edf96d6e4dc81aea2d8490a7e9db177bf8acb0e951c24940bf866cb313f" 670 | "checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" 671 | "checksum term 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" 672 | "checksum termbox-sys 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "afbf597ba2137c0f99b2675988701dc1cd70c2aaa213002a133b08e08fbf42ee" 673 | "checksum thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03" 674 | "checksum thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "8576dbbfcaef9641452d5cf0df9b0e7eeab7694956dd33bb61515fb8f18cfdd5" 675 | "checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" 676 | "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" 677 | "checksum utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f" 678 | "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 679 | "checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770" 680 | "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 681 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 682 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 683 | "checksum x11-clipboard 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2e7374c7699210cca7084ca61d57e09640fc744d1391808cb9ae2fe4ca9bd1df" 684 | "checksum xcb 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de" 685 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "m-editor" 3 | version = "0.1.0" 4 | description = "A simple text editor" 5 | license = "MIT" 6 | homepage = "https://github.com/jinfagang/m" 7 | repository = "https://github.com/jinfagang/m" 8 | keywords = ["editor", "text", "terminal"] 9 | readme = "README.md" 10 | authors = ["Jin Fagang "] 11 | 12 | [dependencies] 13 | gapbuffer = "0.1.1" 14 | unicode-width = "0.1.1" 15 | clippy = {version = "*", optional = true} 16 | regex = "0.1" 17 | lazy_static = "0.2" 18 | clipboard = "0.4.6" 19 | 20 | 21 | [dependencies.rustbox] 22 | git = "https://github.com/gchp/rustbox" 23 | 24 | [lib] 25 | name = "m" 26 | path = "src/m/lib.rs" 27 | 28 | [[bin]] 29 | path = "src/main.rs" 30 | name = "m" 31 | doc = false 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014-2015 Greg Chapple 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # m editor 2 | 3 | ![](https://s2.ax1x.com/2019/07/03/ZYcMPx.png) 4 | 5 | 6 | 7 | **m** editor written in **rust**, it's *modern and fast*. *m* means minimal and modern, vims or emacs are just too old and hard to use. Think about how many time wasted when you try to save a file need permissions of sudo. 8 | So here we re-written a text editor in pure *rust* and designed a pretty new and modern editor which can be cross platform. Our dreaming eidtor is a weapon, so it must be simply and straight forward to use. 9 | Below is the final design of our brand new *m editor*: 10 | 11 | - `ctrl + s` to save; 12 | - `ctrl + q` to quit (save and quit); 13 | - `ctrl + o` to force quit without save; 14 | - `ctrl + b` jump curse to begin (like terminal); 15 | - `ctrl + e` jump curse to end; 16 | - `ctrl + d` copy current line to next; 17 | - `ctrl + k` delete current line; 18 | - `ctrl + c` copy select content; 19 | - `ctrl + v` paste copied content; 20 | - `ctrl + z` undo the last operation; 21 | 22 | 23 | 24 | Above shortcuts were already implemented in **m**! Just start and check it out, you can easily do your modification on *m*! 25 | 26 | 27 | **note**: 28 | This README is edit with m, and it's very comfortable than vim!! I am really welcome rustians send me PR if you are interested in build a self-defined editor! Do u support our brand new design of *m*? 😆 it should save lots of time in this way!! And very much intuitive. 29 | 30 | 31 | 32 | 33 | ## Install 34 | 35 | You should install rust and cargo, so that it can be built like this: 36 | 37 | ``` 38 | cargo build --release 39 | ``` 40 | if you got some error of `#![feature] may not be used on stable channel`, you can simply do this: 41 | 42 | ``` 43 | rustup override set nightly 44 | ``` 45 | 46 | apt-get and brew install will coming soon... 47 | 48 | 49 | 50 | ## Todo 51 | 52 | We continues make function fully support of *m*, also make it easy to install from `apt-get` or `brew`. We will have those function does not support yet, if you interested, welcome to PR! 53 | 54 | - [ ] `ctrl + a` select all; 55 | - [ ] fix some bug; 56 | - [ ] using shift and arrow key to select text; 57 | - [ ] maybe merges more function to m; 58 | - [ ] new create file when filename does not exist; 59 | - [ ] Adding hightlights to certain content such as codes and markdowns. 60 | 61 | again, if you want learn rust and like to written a terminal editor with yourself defined shortcuts, you can start **m** and fork it!! 62 | 63 | 64 | ## Copyright 65 | 66 | this work is original done by: `gchp`, I just did some modifications and fit to modern editors. -------------------------------------------------------------------------------- /src/m/bindings.rs: -------------------------------------------------------------------------------- 1 | use buffer::Mark; 2 | use command::{Action, Command, Operation}; 3 | use key::Key; 4 | use textobject::{Anchor, Kind, Offset, TextObject}; 5 | 6 | pub fn handle_key_event(key: Key) -> Command { 7 | match key { 8 | // Editor Commands 9 | Key::Ctrl('q') => Command::exit_editor(), 10 | Key::Ctrl('s') => Command::save_buffer(), 11 | Key::Ctrl('o') => Command::force_exit_editor(), 12 | 13 | Key::Ctrl('d') => Command::duplicate_selection(), 14 | Key::Ctrl('k') => Command::delete_selection(), 15 | Key::Ctrl('x') => Command::cut_selection(), 16 | Key::Ctrl('c') => Command::copy_selection(), 17 | Key::Ctrl('v') => Command::paste(), 18 | Key::CtrlUp => Command::move_selection(false), 19 | Key::CtrlDown => Command::move_selection(true), 20 | 21 | Key::Ctrl('z') => Command::undo(), 22 | Key::Ctrl('y') => Command::redo(), 23 | 24 | 25 | Key::Char(c) => Command::insert_char(c), 26 | 27 | // Cursor movement 28 | Key::Up => Command::movement( 29 | Offset::Backward(1, Mark::Cursor(0)), 30 | Kind::Line(Anchor::Same), 31 | ), 32 | 33 | Key::Down => Command::movement( 34 | Offset::Forward(1, Mark::Cursor(0)), 35 | Kind::Line(Anchor::Same), 36 | ), 37 | 38 | Key::PgUp => Command::movement( 39 | Offset::Backward(10, Mark::Cursor(0)), 40 | Kind::Line(Anchor::Same), 41 | ), 42 | 43 | Key::PgDown => Command::movement( 44 | Offset::Forward(10, Mark::Cursor(0)), 45 | Kind::Line(Anchor::Same), 46 | ), 47 | 48 | Key::Left => Command::movement(Offset::Backward(1, Mark::Cursor(0)), Kind::Char), 49 | Key::Right => Command::movement(Offset::Forward(1, Mark::Cursor(0)), Kind::Char), 50 | 51 | Key::CtrlRight => Command::movement( 52 | Offset::Forward(1, Mark::Cursor(0)), 53 | Kind::Word(Anchor::Start), 54 | ), 55 | Key::CtrlLeft => Command::movement( 56 | Offset::Backward(1, Mark::Cursor(0)), 57 | Kind::Word(Anchor::Start), 58 | ), 59 | 60 | Key::End => Command::movement(Offset::Forward(0, Mark::Cursor(0)), Kind::Line(Anchor::End)), 61 | Key::Home => Command::movement( 62 | Offset::Backward(0, Mark::Cursor(0)), 63 | Kind::Line(Anchor::Start), 64 | ), 65 | 66 | // Editing 67 | Key::Tab => Command::insert_tab(), 68 | Key::Enter => Command::insert_char('\n'), 69 | Key::Backspace => Command { 70 | number: 1, 71 | action: Action::Operation(Operation::DeleteFromMark(Mark::Cursor(0))), 72 | object: Some(TextObject { 73 | kind: Kind::Char, 74 | offset: Offset::Backward(1, Mark::Cursor(0)), 75 | }), 76 | }, 77 | Key::Delete => Command { 78 | number: 1, 79 | action: Action::Operation(Operation::DeleteFromMark(Mark::Cursor(0))), 80 | object: Some(TextObject { 81 | kind: Kind::Char, 82 | offset: Offset::Forward(1, Mark::Cursor(0)), 83 | }), 84 | }, 85 | 86 | 87 | // History 88 | _ => Command::noop(), 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/m/buffer.rs: -------------------------------------------------------------------------------- 1 | //FIXME: Check unicode support 2 | // stdlib dependencies 3 | use std::cmp; 4 | use std::collections::HashMap; 5 | use std::convert::From; 6 | use std::fs::File; 7 | use std::io::{Read, Stdin}; 8 | use std::path::PathBuf; 9 | 10 | // external dependencies 11 | use gapbuffer::GapBuffer; 12 | 13 | // local dependencies 14 | use input::Input; 15 | use iterators::Lines; 16 | use log::{Change, Log, LogEntry}; 17 | use textobject::{Anchor, Kind, Offset, TextObject}; 18 | use utils; 19 | 20 | #[derive(PartialEq, Debug)] 21 | pub struct MarkPosition { 22 | pub absolute: usize, 23 | absolute_line_start: usize, 24 | line_number: usize, 25 | } 26 | 27 | impl MarkPosition { 28 | fn start() -> MarkPosition { 29 | MarkPosition { 30 | absolute: 0, 31 | line_number: 0, 32 | absolute_line_start: 0, 33 | } 34 | } 35 | } 36 | 37 | impl From<(usize, usize, usize)> for MarkPosition { 38 | fn from(tuple: (usize, usize, usize)) -> MarkPosition { 39 | let mut mark_pos = MarkPosition::start(); 40 | 41 | mark_pos.absolute = tuple.0; 42 | mark_pos.absolute_line_start = tuple.1; 43 | mark_pos.line_number = tuple.2; 44 | 45 | mark_pos 46 | } 47 | } 48 | 49 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 50 | pub enum Mark { 51 | /// For keeping track of cursors. 52 | Cursor(usize), 53 | 54 | /// For using in determining some display of characters 55 | DisplayMark(usize), 56 | } 57 | 58 | #[derive(Copy, Clone, PartialEq, Eq, Debug)] 59 | pub enum WordEdgeMatch { 60 | Whitespace, 61 | } 62 | 63 | pub struct Buffer { 64 | /// Current buffers text 65 | text: GapBuffer, 66 | 67 | /// Table of marked indices in the text 68 | marks: HashMap, 69 | 70 | /// Transaction history (used for undo/redo) 71 | pub log: Log, 72 | 73 | /// Location on disk where the current buffer should be written 74 | pub file_path: Option, 75 | 76 | /// Whether or not the Buffer has unsaved changes 77 | pub dirty: bool, 78 | } 79 | 80 | #[cfg_attr(feature = "clippy", allow(len_without_is_empty))] 81 | impl Buffer { 82 | /// Constructor for empty buffer. 83 | pub fn new() -> Buffer { 84 | Buffer { 85 | file_path: None, 86 | text: GapBuffer::new(), 87 | marks: HashMap::new(), 88 | log: Log::new(), 89 | dirty: false, 90 | } 91 | } 92 | 93 | /// Length of the text stored in this buffer. 94 | pub fn len(&self) -> usize { 95 | self.text.len() + 1 96 | } 97 | 98 | /// The x,y coordinates of a mark within the file. None if not a valid mark. 99 | pub fn get_mark_display_coords(&self, mark: Mark) -> Option<(usize, usize)> { 100 | if let Some(mark_pos) = self.marks.get(&mark) { 101 | return Some(( 102 | mark_pos.absolute - mark_pos.absolute_line_start, 103 | mark_pos.line_number, 104 | )); 105 | } 106 | 107 | None 108 | } 109 | 110 | /// The absolute index of a mark within the file. None if not a valid mark. 111 | pub fn get_mark_idx(&self, mark: Mark) -> Option { 112 | if let Some(mark_pos) = self.marks.get(&mark) { 113 | if mark_pos.absolute < self.len() { 114 | Some(mark_pos.absolute) 115 | } else { 116 | None 117 | } 118 | } else { 119 | None 120 | } 121 | } 122 | 123 | /// Creates an iterator on the text by lines. 124 | pub fn lines(&self) -> Lines { 125 | Lines { 126 | buffer: &self.text, 127 | tail: 0, 128 | head: self.len(), 129 | } 130 | } 131 | 132 | /// Creates an iterator on the text by lines that begins at the specified mark. 133 | pub fn lines_from(&self, mark: Mark) -> Option { 134 | if let Some(mark_pos) = self.marks.get(&mark) { 135 | if mark_pos.absolute < self.len() { 136 | return Some(Lines { 137 | buffer: &self.text, 138 | tail: mark_pos.absolute, 139 | head: self.len(), 140 | }); 141 | } 142 | } 143 | 144 | None 145 | } 146 | 147 | /// Return the buffer index of a TextObject 148 | pub fn get_object_index(&self, obj: TextObject) -> Option { 149 | match obj.kind { 150 | Kind::Char => self.get_char_index(obj.offset), 151 | Kind::Line(anchor) => self.get_line_index(obj.offset, anchor), 152 | Kind::Word(anchor) => self.get_word_index(obj.offset, anchor), 153 | Kind::Selection(anchor) => self.get_line_index(obj.offset, anchor), 154 | } 155 | } 156 | 157 | /// Get the position of a specific character in the buffer 158 | /// 159 | /// This character can be at an absolute position, or a postion relative 160 | /// to a given mark. 161 | /// 162 | /// ie: get the index of the 7th character after the cursor 163 | /// or: get the index of the 130th character from the start of the buffer 164 | fn get_char_index(&self, offset: Offset) -> Option { 165 | let text = &self.text; 166 | 167 | match offset { 168 | // get the index of the char `offset` chars in front of `mark` 169 | // 170 | // ie: get the index of the char which is X chars in front of the MARK 171 | // or: get the index of the char which is 5 chars in front of the Cursor 172 | Offset::Forward(offset, from_mark) => { 173 | let last = self.len() - 1; 174 | if let Some(mark_pos) = self.marks.get(&from_mark) { 175 | let new_absolute_position = mark_pos.absolute + offset; 176 | if new_absolute_position < last { 177 | // FIXME: it would be nice if we could avoid using get_line_info here... 178 | let new_mark_pos = get_line_info(new_absolute_position, text).unwrap(); 179 | return Some(new_mark_pos); 180 | } else { 181 | // FIXME: it would be nice if we could avoid using get_line_info here... 182 | let new_mark_pos = get_line_info(last, text).unwrap(); 183 | return Some(new_mark_pos); 184 | } 185 | } 186 | 187 | None 188 | } 189 | 190 | // get the index of the char `offset` chars before of `mark` 191 | // 192 | // ie: get the index of the char which is X chars before the MARK 193 | // or: get the index of the char which is 5 chars before the Cursor 194 | Offset::Backward(offset, from_mark) => { 195 | if let Some(mark_pos) = self.marks.get(&from_mark) { 196 | if mark_pos.absolute >= offset { 197 | let new_absolute_position = mark_pos.absolute - offset; 198 | // FIXME: it would be nice if we could avoid using get_line_info here... 199 | let new_mark_pos = get_line_info(new_absolute_position, text).unwrap(); 200 | return Some(new_mark_pos); 201 | } else { 202 | return None; 203 | } 204 | } 205 | 206 | None 207 | } 208 | 209 | // get the index of the char at position `offset` in the buffer 210 | // 211 | // ie: get the index of the 5th char in the buffer 212 | Offset::Absolute(absolute_char_offset) => { 213 | let mut mark_pos = MarkPosition::start(); 214 | mark_pos.absolute = absolute_char_offset; 215 | Some(mark_pos) 216 | } 217 | } 218 | } 219 | 220 | /// Get the position of a specific line in the buffer 221 | /// 222 | /// This line can be at an absolute position, or a postion relative 223 | /// to a given mark. 224 | /// 225 | /// The index is calculated based on a given Anchor. This Anchor determines 226 | /// where in the line the index is calculated. For instance, if you want 227 | /// the index of the start of the line, you would use Anchor::Start. If you 228 | /// are on the 5th char in a line, and want to get the index of the 5th char 229 | /// in another line, you can use Anchor::Same. 230 | /// 231 | /// ie: get the index of the middle of the 7th line after the cursor 232 | /// or: get the index of the start of the 130th line from the start of the buffer 233 | fn get_line_index(&self, offset: Offset, anchor: Anchor) -> Option { 234 | match offset { 235 | Offset::Forward(offset, from_mark) => { 236 | self.get_line_index_forward(anchor, offset, from_mark) 237 | } 238 | Offset::Backward(offset, from_mark) => { 239 | self.get_line_index_backward(anchor, offset, from_mark) 240 | } 241 | Offset::Absolute(line_number) => self.get_line_index_absolute(anchor, line_number), 242 | } 243 | } 244 | 245 | /// Get the position of the line identified by line_number 246 | /// 247 | /// ie. Get the index of Anchor inside the 23th line in the buffer 248 | /// or: Get the index of the start of the 23th line 249 | fn get_line_index_absolute(&self, anchor: Anchor, line_number: usize) -> Option { 250 | let text = &self.text; 251 | 252 | let nlines = (0..text.len()) 253 | .filter(|i| text[*i] == '\n') 254 | .take(line_number + 1) 255 | .collect::>(); 256 | match anchor { 257 | Anchor::Start => { 258 | let mut mark_pos = MarkPosition::start(); 259 | let line_start = nlines[0] + 1; 260 | 261 | mark_pos.absolute = line_start; 262 | mark_pos.absolute_line_start = line_start; 263 | mark_pos.line_number = line_number - 1; 264 | 265 | Some(mark_pos) 266 | } 267 | 268 | Anchor::End => { 269 | let mut mark_pos = MarkPosition::start(); 270 | let end_offset = nlines[line_number - 1]; 271 | 272 | mark_pos.absolute = end_offset; 273 | 274 | Some(mark_pos) 275 | } 276 | 277 | _ => { 278 | print!("Unhandled line anchor: {:?} ", anchor); 279 | None 280 | } 281 | } 282 | } 283 | 284 | fn get_line_index_backward( 285 | &self, 286 | anchor: Anchor, 287 | offset: usize, 288 | from_mark: Mark, 289 | ) -> Option { 290 | let text = &self.text; 291 | if let Some(mark_pos) = self.marks.get(&from_mark) { 292 | let mut nlines = (0..mark_pos.absolute) 293 | .rev() 294 | .filter(|i| text[*i] == '\n') 295 | .collect::>(); 296 | 297 | let size = if nlines.len() < offset + 1 { 298 | 0 299 | } else { 300 | nlines.len() - offset + 1 301 | }; 302 | 303 | nlines.resize(size, 0); 304 | 305 | match anchor { 306 | // Get the index of the start of the desired line 307 | Anchor::Start => { 308 | let mut new_mark_pos = MarkPosition::start(); 309 | 310 | // if this is the first line in the buffer 311 | if nlines.is_empty() { 312 | return Some(new_mark_pos); 313 | } 314 | 315 | let start_offset = cmp::min( 316 | mark_pos.absolute - mark_pos.absolute_line_start + nlines[offset] + 1, 317 | nlines[offset], 318 | ); 319 | new_mark_pos.absolute = start_offset + 1; 320 | new_mark_pos.line_number = nlines.len(); 321 | new_mark_pos.absolute_line_start = nlines[0] + 1; 322 | 323 | Some(new_mark_pos) 324 | } 325 | 326 | // ie. If the current line_index is 5, then the line_index 327 | // returned will be the fifth index from the start of the 328 | // desired line. 329 | Anchor::Same => { 330 | let mut new_mark_pos = MarkPosition::start(); 331 | 332 | if offset == nlines.len() { 333 | new_mark_pos.absolute = 334 | cmp::min(mark_pos.absolute - mark_pos.absolute_line_start, nlines[0]); 335 | } else if offset > nlines.len() || offset == 0 { 336 | return Some(new_mark_pos); 337 | } else { 338 | new_mark_pos.absolute = cmp::min( 339 | mark_pos.absolute - mark_pos.absolute_line_start + nlines[offset] + 1, 340 | nlines[offset - 1], 341 | ); 342 | new_mark_pos.line_number = mark_pos.line_number - offset; 343 | new_mark_pos.absolute_line_start = nlines[nlines.len() - 1] + 1; 344 | } 345 | 346 | Some(new_mark_pos) 347 | } 348 | 349 | _ => { 350 | print!("Unhandled line anchor: {:?} ", anchor); 351 | None 352 | } 353 | } 354 | } else { 355 | None 356 | } 357 | } 358 | 359 | fn get_line_index_forward( 360 | &self, 361 | anchor: Anchor, 362 | offset: usize, 363 | from_mark: Mark, 364 | ) -> Option { 365 | let text = &self.text; 366 | let last = self.len() - 1; 367 | if let Some(mark_pos) = self.marks.get(&from_mark) { 368 | let nlines = (mark_pos.absolute..text.len()) 369 | .filter(|i| text[*i] == '\n') 370 | .take(offset + 1) 371 | .collect::>(); 372 | if nlines.is_empty() { 373 | let mut mark_pos = MarkPosition::start(); 374 | mark_pos.absolute += last; 375 | return Some(mark_pos); 376 | } 377 | 378 | match anchor { 379 | // Get the same index as the current line_index 380 | // 381 | // ie. If the current line_index is 5, then the line_index 382 | // returned will be the fifth index from the start of the 383 | // desired line. 384 | Anchor::Same => { 385 | let mut new_pos = MarkPosition::start(); 386 | let new_line_start = nlines[0] + 1; 387 | 388 | if offset == nlines.len() { 389 | new_pos.absolute = cmp::min( 390 | mark_pos.absolute - mark_pos.absolute_line_start + nlines[offset - 1] 391 | + 1, 392 | last, 393 | ); 394 | new_pos.absolute_line_start = nlines[offset - 1] + 1; 395 | new_pos.line_number = mark_pos.line_number + offset; 396 | } else if offset > nlines.len() { 397 | new_pos.absolute = last; 398 | new_pos.line_number = (last - new_pos.absolute) + 1; 399 | new_pos.absolute_line_start = new_line_start; 400 | } else { 401 | new_pos.absolute = cmp::min( 402 | mark_pos.absolute - mark_pos.absolute_line_start + nlines[offset - 1] 403 | + 1, 404 | nlines[offset], 405 | ); 406 | new_pos.line_number = mark_pos.line_number + offset; 407 | new_pos.absolute_line_start = new_line_start; 408 | } 409 | 410 | Some(new_pos) 411 | } 412 | 413 | // Get the index of the end of the desired line 414 | Anchor::End => { 415 | // if this is the last line in the buffer 416 | if nlines.is_empty() { 417 | let mut new_mark_pos = MarkPosition::start(); 418 | new_mark_pos.absolute = last; 419 | 420 | return Some(new_mark_pos); 421 | } 422 | let end_offset = cmp::min( 423 | mark_pos.absolute - mark_pos.absolute_line_start + nlines[offset] + 1, 424 | nlines[offset], 425 | ); 426 | let mut new_mark_pos = MarkPosition::start(); 427 | new_mark_pos.absolute = end_offset; 428 | new_mark_pos.line_number = mark_pos.line_number; 429 | new_mark_pos.absolute_line_start = mark_pos.absolute_line_start; 430 | 431 | Some(new_mark_pos) 432 | } 433 | 434 | _ => { 435 | print!("Unhandled line anchor: {:?} ", anchor); 436 | None 437 | } 438 | } 439 | } else { 440 | None 441 | } 442 | } 443 | 444 | fn get_word_index(&self, offset: Offset, anchor: Anchor) -> Option { 445 | match offset { 446 | Offset::Forward(nth_word, from_mark) => { 447 | self.get_word_index_forward(anchor, nth_word, from_mark) 448 | } 449 | Offset::Backward(nth_word, from_mark) => { 450 | self.get_word_index_backward(anchor, nth_word, from_mark) 451 | } 452 | Offset::Absolute(word_number) => self.get_word_index_absolute(anchor, word_number), 453 | } 454 | } 455 | 456 | fn get_word_index_forward( 457 | &self, 458 | anchor: Anchor, 459 | nth_word: usize, 460 | from_mark: Mark, 461 | ) -> Option { 462 | let text = &self.text; 463 | let last = self.len() - 1; 464 | // TODO: use anchor to determine this 465 | let edger = WordEdgeMatch::Whitespace; 466 | 467 | if let Some(mark_pos) = self.marks.get(&from_mark) { 468 | match anchor { 469 | Anchor::Start => match get_words(mark_pos.absolute, nth_word, edger, text) { 470 | Some(new_index) => { 471 | let new_mark_pos = get_line_info(new_index, text).unwrap(); 472 | return Some(new_mark_pos); 473 | } 474 | None => { 475 | let new_mark_pos = get_line_info(last, text).unwrap(); 476 | return Some(new_mark_pos); 477 | } 478 | }, 479 | 480 | _ => { 481 | eprint!("Unhandled word anchor: {:?} ", anchor); 482 | let mut new_mark_pos = MarkPosition::start(); 483 | new_mark_pos.absolute = last; 484 | 485 | return Some(new_mark_pos); 486 | } 487 | } 488 | } 489 | 490 | None 491 | } 492 | 493 | fn get_word_index_backward( 494 | &self, 495 | anchor: Anchor, 496 | nth_word: usize, 497 | from_mark: Mark, 498 | ) -> Option { 499 | let text = &self.text; 500 | let last = self.len() - 1; 501 | 502 | // TODO: use anchor to determine this 503 | let edger = WordEdgeMatch::Whitespace; 504 | 505 | if let Some(mark_pos) = self.marks.get(&from_mark) { 506 | match anchor { 507 | Anchor::Start => { 508 | // move to the start of the nth_word before the mark 509 | match get_words_rev(mark_pos.absolute, nth_word, edger, text) { 510 | Some(new_index) => { 511 | let new_mark_pos = get_line_info(new_index, text).unwrap(); 512 | return Some(new_mark_pos); 513 | } 514 | None => { 515 | return Some(MarkPosition::start()); 516 | } 517 | } 518 | } 519 | 520 | _ => { 521 | eprint!("Unhandled word anchor: {:?} ", anchor); 522 | let mut new_mark_pos = MarkPosition::start(); 523 | new_mark_pos.absolute = last; 524 | return Some(new_mark_pos); 525 | } 526 | } 527 | } 528 | 529 | None 530 | } 531 | 532 | fn get_word_index_absolute(&self, anchor: Anchor, word_number: usize) -> Option { 533 | let text = &self.text; 534 | // TODO: use anchor to determine this 535 | let edger = WordEdgeMatch::Whitespace; 536 | 537 | match anchor { 538 | Anchor::Start => { 539 | let new_index = get_words(0, word_number - 1, edger, text).unwrap(); 540 | 541 | // let mut new_mark_pos = MarkPosition::start(); 542 | // new_mark_pos.absolute = new_index; 543 | // new_mark_pos.line_start_offset = new_index - get_line(new_index, text).unwrap(); 544 | let new_mark_pos = get_line_info(new_index, text).unwrap(); 545 | 546 | Some(new_mark_pos) 547 | } 548 | 549 | _ => { 550 | print!("Unhandled word anchor: {:?} ", anchor); 551 | None 552 | } 553 | } 554 | } 555 | 556 | /// Returns the status text for this buffer. 557 | pub fn status_text(&self) -> String { 558 | match self.file_path { 559 | Some(ref path) => format!("[{}] ", path.display()), 560 | None => "untitled ".into(), 561 | } 562 | } 563 | 564 | /// Sets the mark to the location of a given TextObject, if it exists. 565 | /// Adds a new mark or overwrites an existing mark. 566 | pub fn set_mark_to_object(&mut self, mark: Mark, obj: TextObject) { 567 | if let Some(mark_pos) = self.get_object_index(obj) { 568 | self.set_mark(mark, mark_pos.absolute); 569 | } 570 | } 571 | 572 | /// Sets the mark to a given absolute index. Adds a new mark or overwrites an existing mark. 573 | pub fn set_mark(&mut self, mark: Mark, idx: usize) { 574 | if let Some(mark_pos) = get_line_info(idx, &self.text) { 575 | if let Some(existing_pos) = self.marks.get_mut(&mark) { 576 | existing_pos.absolute = mark_pos.absolute; 577 | existing_pos.line_number = mark_pos.line_number; 578 | existing_pos.absolute_line_start = mark_pos.absolute_line_start; 579 | return; 580 | } 581 | self.marks.insert(mark, mark_pos); 582 | } 583 | } 584 | 585 | // Remove the chars in the range from start to end 586 | pub fn remove_range(&mut self, start: usize, end: usize) -> Option> { 587 | self.dirty = true; 588 | let text = &mut self.text; 589 | let mut transaction = self.log.start(start); 590 | let mut vec = (start..end) 591 | .rev() 592 | .filter_map(|idx| text.remove(idx).map(|ch| (idx, ch))) 593 | .inspect(|&(idx, ch)| transaction.log(Change::Remove(idx, ch), idx)) 594 | .map(|(_, ch)| ch) 595 | .collect::>(); 596 | vec.reverse(); 597 | Some(vec) 598 | } 599 | 600 | // Remove the chars between mark and object 601 | pub fn remove_from_mark_to_object( 602 | &mut self, 603 | mark: Mark, 604 | object: TextObject, 605 | ) -> Option> { 606 | let (start, end) = { 607 | let mark_pos = &self.marks[&mark]; 608 | let obj_pos = self.get_object_index(object).unwrap(); 609 | 610 | if mark_pos.absolute < obj_pos.absolute { 611 | (mark_pos.absolute, obj_pos.absolute) 612 | } else { 613 | (obj_pos.absolute, mark_pos.absolute) 614 | } 615 | }; 616 | self.remove_range(start, end) 617 | } 618 | 619 | // Get the chars in the range from start to end 620 | // TODO: This needs out of bounds handling 621 | pub fn get_range(&mut self, start: usize, end: usize) -> Option> { 622 | let mut vec = (start..end) 623 | .rev() 624 | .map(|idx| *self.text.get(idx).unwrap()) 625 | .collect::>(); 626 | vec.reverse(); 627 | Some(vec) 628 | } 629 | 630 | /// Insert a string at the mark. 631 | pub fn insert_string(&mut self, mark: Mark, s: String) -> Option { 632 | let mut len = 0; 633 | 634 | let mut transaction = self.log.start(self.marks.get(&mark).unwrap().absolute); 635 | if let Some(mark_pos) = self.marks.get(&mark) { 636 | for ch in s.chars().rev() { 637 | if ch == '\t' { 638 | for _ in 0..4 { 639 | self.text.insert(mark_pos.absolute, ' '); 640 | transaction.log(Change::Insert(mark_pos.absolute, ch), mark_pos.absolute); 641 | } 642 | len += 4; 643 | } else { 644 | self.text.insert(mark_pos.absolute, ch); 645 | 646 | len += utils::char_width(ch, false, 4, 1).unwrap(); 647 | transaction.log(Change::Insert(mark_pos.absolute, ch), mark_pos.absolute); 648 | } 649 | } 650 | } 651 | 652 | self.dirty = true; 653 | Some(len) 654 | } 655 | 656 | /// Redo most recently undone action. 657 | pub fn redo(&mut self) -> Option<&LogEntry> { 658 | if let Some(transaction) = self.log.redo() { 659 | commit(transaction, &mut self.text); 660 | Some(transaction) 661 | } else { 662 | None 663 | } 664 | } 665 | 666 | /// Undo most recently performed action. 667 | pub fn undo(&mut self) -> Option<&LogEntry> { 668 | if let Some(transaction) = self.log.undo() { 669 | commit(transaction, &mut self.text); 670 | Some(transaction) 671 | } else { 672 | None 673 | } 674 | } 675 | } 676 | 677 | // This is a bit of a hack to get around an error I was getting when 678 | // implementing From for Buffer with From for Buffer. 679 | // The compiler was telling me this was a conflicting implementation even 680 | // though Read is not implemented for PathBuf. Changing R: Read to 681 | // R: Read + BufferFrom fixes the error. 682 | // 683 | // TODO: investigate this further - possible compiler bug? 684 | pub trait BufferFrom {} 685 | impl BufferFrom for Stdin {} 686 | impl BufferFrom for File {} 687 | 688 | impl From for Buffer { 689 | fn from(path: PathBuf) -> Buffer { 690 | match File::open(&path) { 691 | Ok(file) => { 692 | let mut buf = Buffer::from(file); 693 | buf.file_path = Some(path); 694 | buf 695 | } 696 | Err(_) => Buffer::new(), 697 | } 698 | } 699 | } 700 | 701 | impl From for Buffer { 702 | fn from(mut reader: R) -> Buffer { 703 | let mut buff = Buffer::new(); 704 | let mut contents = String::new(); 705 | if reader.read_to_string(&mut contents).is_ok() { 706 | buff.text.extend(contents.chars()); 707 | } 708 | buff 709 | } 710 | } 711 | 712 | impl From for Buffer { 713 | fn from(input: Input) -> Buffer { 714 | match input { 715 | Input::Filename(path) => match path { 716 | Some(path) => Buffer::from(PathBuf::from(path)), 717 | None => Buffer::new(), 718 | }, 719 | Input::Stdin(reader) => Buffer::from(reader), 720 | } 721 | } 722 | } 723 | 724 | impl WordEdgeMatch { 725 | /// If c1 -> c2 is the start of a word. 726 | /// If end of word matching is wanted then pass the chars in reversed. 727 | fn is_word_edge(&self, c1: &char, c2: &char) -> bool { 728 | // FIXME: unicode support - issue #69 729 | match (self, *c1, *c2) { 730 | (_, '\n', '\n') => true, // Blank lines are always counted as a word 731 | (&WordEdgeMatch::Whitespace, c1, c2) => c1.is_whitespace() && !c2.is_whitespace(), 732 | // (&WordEdgeMatch::Alphabet, c1, c2) if c1.is_whitespace() => !c2.is_whitespace(), 733 | // (&WordEdgeMatch::Alphabet, c1, c2) if is_alpha_or_(c1) => !is_alpha_or_(c2) && !c2.is_whitespace(), 734 | // (&WordEdgeMatch::Alphabet, c1, c2) if !is_alpha_or_(c1) => is_alpha_or_(c2) && !c2.is_whitespace(), 735 | // (&WordEdgeMatch::Alphabet, _, _) => false, 736 | } 737 | } 738 | } 739 | 740 | fn get_words( 741 | mark: usize, 742 | n_words: usize, 743 | edger: WordEdgeMatch, 744 | text: &GapBuffer, 745 | ) -> Option { 746 | let text_len = text.len(); 747 | if text_len == 0 { 748 | return None; 749 | } 750 | 751 | (mark + 1..text_len - 1) 752 | .filter(|idx| edger.is_word_edge(&text[*idx - 1], &text[*idx])) 753 | .take(n_words) 754 | .last() 755 | } 756 | 757 | fn get_words_rev( 758 | mark: usize, 759 | n_words: usize, 760 | edger: WordEdgeMatch, 761 | text: &GapBuffer, 762 | ) -> Option { 763 | (1..mark) 764 | .rev() 765 | .filter(|idx| edger.is_word_edge(&text[*idx - 1], &text[*idx])) 766 | .take(n_words) 767 | .last() 768 | } 769 | 770 | fn get_line_info(mark: usize, text: &GapBuffer) -> Option { 771 | let val = cmp::min(mark, text.len()); 772 | let line_starts: Vec = (0..val + 1) 773 | .rev() 774 | .filter(|idx| *idx == 0 || text[*idx - 1] == '\n') 775 | .collect(); 776 | 777 | if line_starts.is_empty() { 778 | None 779 | } else { 780 | let mut mark_pos = MarkPosition::start(); 781 | mark_pos.absolute_line_start = line_starts[0]; 782 | mark_pos.line_number = line_starts.len() - 1; 783 | mark_pos.absolute = mark; 784 | Some(mark_pos) 785 | } 786 | } 787 | 788 | /// Performs a transaction on the passed in buffer. 789 | fn commit(transaction: &LogEntry, text: &mut GapBuffer) { 790 | for change in &transaction.changes { 791 | match *change { 792 | Change::Insert(idx, ch) => { 793 | text.insert(idx, ch); 794 | } 795 | Change::Remove(idx, _) => { 796 | text.remove(idx); 797 | } 798 | } 799 | } 800 | } 801 | 802 | #[cfg(test)] 803 | mod test { 804 | 805 | use super::get_line_info; 806 | use buffer::{Buffer, Mark, MarkPosition}; 807 | use textobject::{Anchor, Kind, Offset, TextObject}; 808 | 809 | fn setup_buffer(testcase: &'static str) -> Buffer { 810 | let mut buffer = Buffer::new(); 811 | buffer.text.extend(testcase.chars()); 812 | buffer.set_mark(Mark::Cursor(0), 0); 813 | buffer 814 | } 815 | 816 | #[test] 817 | fn move_mark_char_right() { 818 | let mut buffer = setup_buffer("Some test content"); 819 | let mark = Mark::Cursor(0); 820 | let obj = TextObject { 821 | kind: Kind::Char, 822 | offset: Offset::Forward(1, mark), 823 | }; 824 | 825 | buffer.set_mark_to_object(mark, obj); 826 | 827 | assert_eq!( 828 | *buffer.marks.get(&mark).unwrap(), 829 | MarkPosition::from((1, 0, 0)) 830 | ); 831 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (1, 0)); 832 | } 833 | 834 | #[test] 835 | fn move_mark_char_left() { 836 | let mut buffer = setup_buffer("Some test content"); 837 | let mark = Mark::Cursor(0); 838 | let obj = TextObject { 839 | kind: Kind::Char, 840 | offset: Offset::Backward(1, mark), 841 | }; 842 | 843 | buffer.set_mark(mark, 3); 844 | buffer.set_mark_to_object(mark, obj); 845 | 846 | assert_eq!( 847 | *buffer.marks.get(&mark).unwrap(), 848 | MarkPosition::from((2, 0, 0)) 849 | ); 850 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (2, 0)); 851 | } 852 | 853 | #[test] 854 | fn move_mark_five_chars_right() { 855 | let mut buffer = setup_buffer("Some test content"); 856 | let mark = Mark::Cursor(0); 857 | let obj = TextObject { 858 | kind: Kind::Char, 859 | offset: Offset::Forward(5, mark), 860 | }; 861 | 862 | buffer.set_mark_to_object(mark, obj); 863 | 864 | assert_eq!( 865 | *buffer.marks.get(&mark).unwrap(), 866 | MarkPosition::from((5, 0, 0)) 867 | ); 868 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (5, 0)); 869 | } 870 | 871 | #[test] 872 | fn move_mark_line_down() { 873 | let mut buffer = setup_buffer("Some test content\nwith new\nlines!"); 874 | let mark = Mark::Cursor(0); 875 | let obj = TextObject { 876 | kind: Kind::Line(Anchor::Same), 877 | offset: Offset::Forward(1, mark), 878 | }; 879 | 880 | buffer.set_mark_to_object(mark, obj); 881 | 882 | assert_eq!( 883 | *buffer.marks.get(&mark).unwrap(), 884 | MarkPosition::from((18, 18, 1)) 885 | ); 886 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (0, 1)); 887 | } 888 | 889 | #[test] 890 | fn move_mark_line_up() { 891 | let mut buffer = setup_buffer("Some test content\nnew lines!"); 892 | let mark = Mark::Cursor(0); 893 | let obj = TextObject { 894 | kind: Kind::Line(Anchor::Same), 895 | offset: Offset::Backward(1, mark), 896 | }; 897 | 898 | buffer.set_mark(mark, 18); 899 | buffer.set_mark_to_object(mark, obj); 900 | 901 | assert_eq!( 902 | *buffer.marks.get(&mark).unwrap(), 903 | MarkPosition::from((0, 0, 0)) 904 | ); 905 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (0, 0)); 906 | } 907 | 908 | #[test] 909 | fn move_mark_two_lines_down() { 910 | let mut buffer = setup_buffer("Some test content\nwith new\nlines!"); 911 | let mark = Mark::Cursor(0); 912 | let obj = TextObject { 913 | kind: Kind::Line(Anchor::Same), 914 | offset: Offset::Forward(2, mark), 915 | }; 916 | 917 | buffer.set_mark_to_object(mark, obj); 918 | 919 | assert_eq!( 920 | *buffer.marks.get(&mark).unwrap(), 921 | MarkPosition::from((27, 27, 2)) 922 | ); 923 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (0, 2)); 924 | } 925 | 926 | // FIXME: re-enable this 927 | #[test] 928 | #[ignore] 929 | fn move_mark_line_down_to_shorter_line() { 930 | let mut buffer = setup_buffer("Some test content\nwith new\nlines!"); 931 | let mark = Mark::Cursor(0); 932 | let obj = TextObject { 933 | kind: Kind::Line(Anchor::Same), 934 | offset: Offset::Forward(1, mark), 935 | }; 936 | 937 | buffer.set_mark(mark, 15); 938 | buffer.set_mark_to_object(mark, obj); 939 | 940 | assert_eq!( 941 | *buffer.marks.get(&mark).unwrap(), 942 | MarkPosition::from((26, 8, 1)) 943 | ); 944 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (8, 1)); 945 | 946 | let obj = TextObject { 947 | kind: Kind::Line(Anchor::Same), 948 | offset: Offset::Backward(1, mark), 949 | }; 950 | buffer.set_mark_to_object(mark, obj); 951 | 952 | // NOTE: this test could be wrong... 953 | assert_eq!( 954 | *buffer.marks.get(&mark).unwrap(), 955 | MarkPosition::from((15, 15, 0)) 956 | ); 957 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (15, 0)); 958 | } 959 | 960 | #[test] 961 | fn move_mark_two_words_right() { 962 | let mut buffer = setup_buffer("Some test content\nwith new\nlines!"); 963 | let mark = Mark::Cursor(0); 964 | let obj = TextObject { 965 | kind: Kind::Word(Anchor::Start), 966 | offset: Offset::Forward(2, mark), 967 | }; 968 | 969 | buffer.set_mark_to_object(mark, obj); 970 | 971 | assert_eq!( 972 | *buffer.marks.get(&mark).unwrap(), 973 | MarkPosition::from((10, 0, 0)) 974 | ); 975 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (10, 0)); 976 | } 977 | 978 | #[test] 979 | fn move_mark_two_words_left() { 980 | let mut buffer = setup_buffer("Some test content\nwith new\nlines!"); 981 | let mark = Mark::Cursor(0); 982 | let obj = TextObject { 983 | kind: Kind::Word(Anchor::Start), 984 | offset: Offset::Backward(2, mark), 985 | }; 986 | 987 | buffer.set_mark(mark, 18); 988 | buffer.set_mark_to_object(mark, obj); 989 | 990 | assert_eq!( 991 | *buffer.marks.get(&mark).unwrap(), 992 | MarkPosition::from((5, 0, 0)) 993 | ); 994 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (5, 0)); 995 | } 996 | 997 | #[test] 998 | fn move_mark_move_word_left_at_start_of_buffer() { 999 | let mut buffer = setup_buffer("Some test content\nwith new\nlines!"); 1000 | let mark = Mark::Cursor(0); 1001 | let obj = TextObject { 1002 | kind: Kind::Word(Anchor::Start), 1003 | offset: Offset::Backward(1, mark), 1004 | }; 1005 | 1006 | buffer.set_mark(mark, 5); 1007 | buffer.set_mark_to_object(mark, obj); 1008 | 1009 | assert_eq!( 1010 | *buffer.marks.get(&mark).unwrap(), 1011 | MarkPosition::from((0, 0, 0)) 1012 | ); 1013 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (0, 0)); 1014 | } 1015 | 1016 | #[test] 1017 | fn move_mark_move_word_right_past_end_of_buffer() { 1018 | let mut buffer = setup_buffer("Some test content\nwith new\nlines!"); 1019 | let mark = Mark::Cursor(0); 1020 | let obj = TextObject { 1021 | kind: Kind::Word(Anchor::Start), 1022 | offset: Offset::Forward(8, mark), 1023 | }; 1024 | 1025 | buffer.set_mark(mark, 28); 1026 | buffer.set_mark_to_object(mark, obj); 1027 | 1028 | assert_eq!( 1029 | *buffer.marks.get(&mark).unwrap(), 1030 | MarkPosition::from((33, 27, 2)) 1031 | ); 1032 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (6, 2)); 1033 | } 1034 | 1035 | #[test] 1036 | fn move_mark_second_word_in_buffer() { 1037 | let mut buffer = setup_buffer("Some test content\nwith new\nlines!"); 1038 | let mark = Mark::Cursor(0); 1039 | let obj = TextObject { 1040 | kind: Kind::Word(Anchor::Start), 1041 | offset: Offset::Absolute(2), 1042 | }; 1043 | 1044 | buffer.set_mark(mark, 18); 1045 | buffer.set_mark_to_object(mark, obj); 1046 | 1047 | assert_eq!( 1048 | *buffer.marks.get(&mark).unwrap(), 1049 | MarkPosition::from((5, 0, 0)) 1050 | ); 1051 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (5, 0)); 1052 | } 1053 | 1054 | #[test] 1055 | fn move_mark_fifth_word_in_buffer() { 1056 | let mut buffer = setup_buffer("Some test content\nwith new\nlines!"); 1057 | let mark = Mark::Cursor(0); 1058 | let obj = TextObject { 1059 | kind: Kind::Word(Anchor::Start), 1060 | offset: Offset::Absolute(5), 1061 | }; 1062 | 1063 | buffer.set_mark(mark, 18); 1064 | buffer.set_mark_to_object(mark, obj); 1065 | 1066 | assert_eq!( 1067 | *buffer.marks.get(&mark).unwrap(), 1068 | MarkPosition::from((23, 18, 1)) 1069 | ); 1070 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (5, 1)); 1071 | } 1072 | 1073 | #[test] 1074 | fn move_mark_second_line_in_buffer() { 1075 | let mut buffer = setup_buffer("Some test content\nwith new\nlines!"); 1076 | let mark = Mark::Cursor(0); 1077 | let obj = TextObject { 1078 | kind: Kind::Line(Anchor::Start), 1079 | offset: Offset::Absolute(2), 1080 | }; 1081 | 1082 | buffer.set_mark_to_object(mark, obj); 1083 | 1084 | assert_eq!( 1085 | *buffer.marks.get(&mark).unwrap(), 1086 | MarkPosition::from((18, 18, 1)) 1087 | ); 1088 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (0, 1)); 1089 | } 1090 | 1091 | #[test] 1092 | fn move_mark_second_char_in_buffer() { 1093 | let mut buffer = setup_buffer("Some test content\nwith new\nlines!"); 1094 | let mark = Mark::Cursor(0); 1095 | let obj = TextObject { 1096 | kind: Kind::Char, 1097 | offset: Offset::Absolute(2), 1098 | }; 1099 | 1100 | buffer.set_mark(mark, 18); 1101 | buffer.set_mark_to_object(mark, obj); 1102 | 1103 | assert_eq!( 1104 | *buffer.marks.get(&mark).unwrap(), 1105 | MarkPosition::from((2, 0, 0)) 1106 | ); 1107 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (2, 0)); 1108 | } 1109 | 1110 | #[test] 1111 | fn move_mark_end_of_line() { 1112 | let mut buffer = setup_buffer("Some test content\nwith new\nlines!"); 1113 | let mark = Mark::Cursor(0); 1114 | let obj = TextObject { 1115 | kind: Kind::Line(Anchor::End), 1116 | offset: Offset::Forward(0, mark), 1117 | }; 1118 | 1119 | buffer.set_mark(mark, 19); 1120 | buffer.set_mark_to_object(mark, obj); 1121 | 1122 | assert_eq!( 1123 | *buffer.marks.get(&mark).unwrap(), 1124 | MarkPosition::from((26, 18, 1)) 1125 | ); 1126 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (8, 1)); 1127 | } 1128 | 1129 | #[test] 1130 | fn move_mark_start_of_line() { 1131 | let mut buffer = setup_buffer("Some test content\nwith new\nlines!"); 1132 | let mark = Mark::Cursor(0); 1133 | let obj = TextObject { 1134 | kind: Kind::Line(Anchor::Start), 1135 | offset: Offset::Backward(0, mark), 1136 | }; 1137 | 1138 | buffer.set_mark(mark, 19); 1139 | buffer.set_mark_to_object(mark, obj); 1140 | 1141 | assert_eq!( 1142 | *buffer.marks.get(&mark).unwrap(), 1143 | MarkPosition::from((18, 18, 1)) 1144 | ); 1145 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (0, 1)); 1146 | } 1147 | 1148 | #[test] 1149 | fn move_mark_past_last_line() { 1150 | let mut buffer = setup_buffer("Some test content\n"); 1151 | let mark = Mark::Cursor(0); 1152 | let obj = TextObject { 1153 | kind: Kind::Line(Anchor::Same), 1154 | offset: Offset::Forward(6, mark), 1155 | }; 1156 | 1157 | buffer.set_mark_to_object(mark, obj); 1158 | 1159 | assert_eq!( 1160 | *buffer.marks.get(&mark).unwrap(), 1161 | MarkPosition::from((18, 18, 1)) 1162 | ); 1163 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (0, 1)); 1164 | } 1165 | 1166 | #[test] 1167 | fn move_mark_line_up_middle_of_file() { 1168 | let mut buffer = setup_buffer("Some\ntest\ncontent"); 1169 | let mark = Mark::Cursor(0); 1170 | let obj = TextObject { 1171 | kind: Kind::Line(Anchor::Same), 1172 | offset: Offset::Backward(1, mark), 1173 | }; 1174 | 1175 | buffer.set_mark(mark, 10); 1176 | buffer.set_mark_to_object(mark, obj); 1177 | 1178 | assert_eq!( 1179 | *buffer.marks.get(&mark).unwrap(), 1180 | MarkPosition::from((5, 5, 1)) 1181 | ); 1182 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (0, 1)); 1183 | } 1184 | 1185 | #[test] 1186 | fn move_mark_line_up_past_first_line() { 1187 | let mut buffer = setup_buffer("Some\ntest\ncontent"); 1188 | let mark = Mark::Cursor(0); 1189 | let obj = TextObject { 1190 | kind: Kind::Line(Anchor::Same), 1191 | offset: Offset::Backward(1, mark), 1192 | }; 1193 | 1194 | buffer.set_mark_to_object(mark, obj); 1195 | 1196 | assert_eq!( 1197 | *buffer.marks.get(&mark).unwrap(), 1198 | MarkPosition::from((0, 0, 0)) 1199 | ); 1200 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (0, 0)); 1201 | } 1202 | 1203 | #[test] 1204 | fn test_insert() { 1205 | let mut buffer = setup_buffer(""); 1206 | buffer.insert_string(Mark::Cursor(0), String::from("A")); 1207 | assert_eq!(buffer.len(), 2); 1208 | assert_eq!(buffer.lines().next().unwrap(), "A"); 1209 | } 1210 | 1211 | #[test] 1212 | fn test_insert_tab() { 1213 | let mut buffer = setup_buffer("test"); 1214 | buffer.insert_string(Mark::Cursor(0), '\t'.to_string()); 1215 | assert_eq!(buffer.lines().next().unwrap(), " test"); 1216 | assert_eq!(buffer.len(), 9); 1217 | } 1218 | 1219 | #[test] 1220 | fn test_insert_newline() { 1221 | let mut buffer = setup_buffer("test"); 1222 | let len = buffer.insert_string(Mark::Cursor(0), '\n'.to_string()); 1223 | assert_eq!(buffer.lines().next().unwrap(), "\n"); 1224 | assert_eq!(buffer.len(), 6); 1225 | assert_eq!(len.unwrap(), 1); 1226 | } 1227 | 1228 | #[test] 1229 | fn test_insert_unicode() { 1230 | let mut buffer = setup_buffer(""); 1231 | buffer.insert_string(Mark::Cursor(0), String::from("Съешь же ещё этих мягких французских булок, да выпей чаю")); 1232 | assert_eq!(buffer.lines().next().unwrap(), "Съешь же ещё этих мягких французских булок, да выпей чаю"); 1233 | assert_eq!(buffer.len(), 57); 1234 | } 1235 | 1236 | #[test] 1237 | fn test_insert_string() { 1238 | let mut buffer = setup_buffer(""); 1239 | let len = buffer.insert_string(Mark::Cursor(0), String::from("insertme")); 1240 | assert_eq!(buffer.lines().next().unwrap(), "insertme"); 1241 | assert_eq!(len.unwrap(), 8); 1242 | } 1243 | 1244 | #[test] 1245 | fn test_get_range() { 1246 | let mut buffer = setup_buffer("some content"); 1247 | 1248 | assert_eq!(buffer.get_range(0, 4).unwrap().len(), 4); 1249 | assert_eq!(buffer.get_range(0, 4).unwrap(), vec!['s', 'o', 'm', 'e']); 1250 | } 1251 | 1252 | #[test] 1253 | fn test_remove() { 1254 | let mut buffer = setup_buffer("ABCD"); 1255 | let mark = Mark::Cursor(0); 1256 | let obj = TextObject { 1257 | kind: Kind::Char, 1258 | offset: Offset::Forward(1, mark), 1259 | }; 1260 | buffer.remove_from_mark_to_object(mark, obj); 1261 | 1262 | assert_eq!(buffer.len(), 4); 1263 | assert_eq!(buffer.lines().next().unwrap(), "BCD"); 1264 | } 1265 | 1266 | #[test] 1267 | fn test_set_mark() { 1268 | let mut buffer = setup_buffer("Test"); 1269 | buffer.set_mark(Mark::Cursor(0), 2); 1270 | 1271 | assert_eq!(buffer.get_mark_idx(Mark::Cursor(0)).unwrap(), 2); 1272 | assert_eq!(buffer.marks.get(&Mark::Cursor(0)).unwrap().absolute, 2); 1273 | } 1274 | 1275 | #[test] 1276 | fn test_to_lines() { 1277 | let buffer = setup_buffer("Test\nA\nTest"); 1278 | let mut lines = buffer.lines(); 1279 | 1280 | assert_eq!(lines.next().unwrap(), "Test\n"); 1281 | assert_eq!(lines.next().unwrap(), "A\n"); 1282 | assert_eq!(lines.next().unwrap(), "Test"); 1283 | } 1284 | 1285 | #[test] 1286 | fn test_to_lines_from() { 1287 | let mut buffer = setup_buffer("Test\nA\nTest"); 1288 | buffer.set_mark(Mark::Cursor(0), 6); 1289 | let mut lines = buffer.lines_from(Mark::Cursor(0)).unwrap(); 1290 | 1291 | assert_eq!(lines.next().unwrap(), "\n"); 1292 | assert_eq!(lines.next().unwrap(), "Test"); 1293 | } 1294 | 1295 | #[test] 1296 | fn test_line_down() { 1297 | let mut buffer = setup_buffer("Some\ntest\ncontent"); 1298 | let mark = Mark::Cursor(0); 1299 | let obj = TextObject { 1300 | kind: Kind::Line(Anchor::Same), 1301 | offset: Offset::Forward(1, mark), 1302 | }; 1303 | 1304 | buffer.set_mark_to_object(mark, obj); 1305 | 1306 | assert_eq!( 1307 | *buffer.marks.get(&mark).unwrap(), 1308 | MarkPosition::from((5, 5, 1)) 1309 | ); 1310 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (0, 1)); 1311 | 1312 | let obj = TextObject { 1313 | kind: Kind::Char, 1314 | offset: Offset::Forward(1, mark), 1315 | }; 1316 | 1317 | buffer.set_mark_to_object(mark, obj); 1318 | 1319 | assert_eq!( 1320 | *buffer.marks.get(&mark).unwrap(), 1321 | MarkPosition::from((6, 5, 1)) 1322 | ); 1323 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (1, 1)); 1324 | } 1325 | 1326 | #[test] 1327 | fn test_get_line_info() { 1328 | let mut buffer = setup_buffer("Test\nA\nTest"); 1329 | buffer.set_mark(Mark::Cursor(0), 10); 1330 | 1331 | let mark_pos = MarkPosition::from((10, 7, 2)); 1332 | 1333 | assert_eq!(mark_pos, get_line_info(10, &buffer.text).unwrap()); 1334 | } 1335 | 1336 | #[test] 1337 | fn get_move_word_forward_emtpy_buffer() { 1338 | let mut buffer = setup_buffer(""); 1339 | let mark = Mark::Cursor(0); 1340 | buffer.set_mark(Mark::Cursor(0), 0); 1341 | 1342 | let obj = TextObject { 1343 | kind: Kind::Word(Anchor::Start), 1344 | offset: Offset::Forward(1, mark), 1345 | }; 1346 | 1347 | buffer.set_mark_to_object(mark, obj); 1348 | 1349 | assert_eq!( 1350 | *buffer.marks.get(&mark).unwrap(), 1351 | MarkPosition::from((0, 0, 0)) 1352 | ); 1353 | assert_eq!(buffer.get_mark_display_coords(mark).unwrap(), (0, 0)); 1354 | } 1355 | 1356 | } 1357 | -------------------------------------------------------------------------------- /src/m/command.rs: -------------------------------------------------------------------------------- 1 | use buffer::Mark; 2 | use textobject::{Kind, Offset, TextObject}; 3 | use command::Operation::Insert; 4 | 5 | 6 | /// Instructions for the Editor. 7 | /// These do NOT alter the text, but may change editor/view state 8 | #[derive(Copy, Clone, Debug)] 9 | pub enum Instruction { 10 | SaveBuffer, 11 | //FindFile, 12 | ExitEditor, 13 | // force exit 14 | ForceExitEditor, 15 | 16 | SetMark(Mark), 17 | ShowMessage(&'static str), 18 | None, 19 | } 20 | 21 | /// Operations on the Buffer. 22 | /// These DO alter the text, but otherwise may NOT change editor/view state 23 | /// Note that these differ from `log::Change` in that they are higher-level 24 | /// operations dependent on state (cursor/mark locations, etc.), as opposed 25 | /// to concrete operations on absolute indexes (insert 'a' at index 158, etc.) 26 | #[derive(Copy, Clone, Debug)] 27 | pub enum Operation { 28 | Insert(char), // insert text 29 | DeleteFromMark(Mark), // delete from some mark to an object 30 | DuplicateSelection, // duplicate the selection 31 | DeleteSelection, // delete the selection 32 | CutSelection, // cut the selection from the buffer to the clipboard 33 | CopySelection, // copy the selection to the clipboard 34 | Paste, // insert the clipboard 35 | MoveSelection(bool), //Move the current selection up or down 36 | 37 | Undo, // rewind buffer transaction log 38 | Redo, // replay buffer transaction log 39 | } 40 | 41 | #[derive(Copy, Clone, Debug)] 42 | pub enum Action { 43 | Operation(Operation), 44 | Instruction(Instruction), 45 | } 46 | 47 | /// A complete, actionable command 48 | #[derive(Copy, Clone, Debug)] 49 | pub struct Command { 50 | pub number: i32, // numeric paramter, line number, repeat count, etc. 51 | pub action: Action, // what to do 52 | pub object: Option, // where to do it 53 | } 54 | 55 | impl Command { 56 | /// Display a message 57 | pub fn show_message(msg: &'static str) -> Command { 58 | Command { 59 | action: Action::Instruction(Instruction::ShowMessage(msg)), 60 | number: 0, 61 | object: None, 62 | } 63 | } 64 | 65 | /// Shortcut to create an ExitEditor command 66 | pub fn exit_editor() -> Command { 67 | Command { 68 | action: Action::Instruction(Instruction::ExitEditor), 69 | number: 0, 70 | object: None, 71 | } 72 | } 73 | 74 | pub fn force_exit_editor() -> Command{ 75 | Command{ 76 | action: Action::Instruction(Instruction::ForceExitEditor), 77 | number: 0, 78 | object: None, 79 | } 80 | } 81 | 82 | /// Shortcut to create a SaveBuffer command 83 | /// number indicates what? it's not 0 here, 0 IS INSTRUCTION AND 1 IS OPERATION 84 | pub fn save_buffer() -> Command { 85 | // save buffer why exit? 86 | Command { 87 | action: Action::Instruction(Instruction::SaveBuffer), 88 | number: 0, 89 | object: None, 90 | } 91 | } 92 | 93 | /// Shortcut to create an Insert command 94 | pub fn insert_char(c: char) -> Command { 95 | Command { 96 | number: 1, 97 | action: Action::Operation(Operation::Insert(c)), 98 | object: None, 99 | } 100 | } 101 | 102 | /// Shortcut to create an Insert command 103 | // FIXME: shouldn't need this method 104 | pub fn insert_tab() -> Command { 105 | Command { 106 | number: 1, 107 | action: Action::Operation(Operation::Insert('\t')), 108 | object: None, 109 | } 110 | } 111 | 112 | /// Shortcut to create DeleteSelection command 113 | pub fn delete_selection() -> Command { 114 | Command { 115 | number: 1, 116 | action: Action::Operation(Operation::DeleteSelection), 117 | object: None, 118 | } 119 | } 120 | 121 | /// Shortcut to create DuplicateSelection command 122 | pub fn duplicate_selection() -> Command { 123 | Command { 124 | number: 1, 125 | action: Action::Operation(Operation::DuplicateSelection), 126 | object: None, 127 | } 128 | } 129 | 130 | /// Shortcut to create CutSelection command 131 | pub fn cut_selection() -> Command { 132 | Command { 133 | number: 1, 134 | action: Action::Operation(Operation::CutSelection), 135 | object: None, 136 | } 137 | } 138 | 139 | /// Shortcut to create Paste command 140 | pub fn paste() -> Command { 141 | Command { 142 | number: 1, 143 | action: Action::Operation(Operation::Paste), 144 | object: None, 145 | } 146 | } 147 | 148 | /// Shortcut to create CopySelection command 149 | pub fn copy_selection() -> Command { 150 | Command { 151 | number: 1, 152 | action: Action::Operation(Operation::CopySelection), 153 | object: None, 154 | } 155 | } 156 | 157 | pub fn move_selection(down: bool) -> Command { 158 | Command { 159 | number: 1, 160 | action: Action::Operation(Operation::MoveSelection(down)), 161 | object: None, 162 | } 163 | } 164 | 165 | /// Shortcut to create Undo command 166 | pub fn undo() -> Command { 167 | Command { 168 | number: 1, 169 | action: Action::Operation(Operation::Undo), 170 | object: None, 171 | } 172 | } 173 | 174 | /// Shortcut to create Redo command 175 | pub fn redo() -> Command { 176 | Command { 177 | number: 1, 178 | action: Action::Operation(Operation::Redo), 179 | object: None, 180 | } 181 | } 182 | 183 | pub fn movement(offset: Offset, kind: Kind) -> Command { 184 | Command { 185 | number: 1, 186 | action: Action::Instruction(Instruction::SetMark(Mark::Cursor(0))), 187 | object: Some(TextObject { 188 | kind: kind, 189 | offset: offset, 190 | }), 191 | } 192 | } 193 | 194 | pub fn noop() -> Command { 195 | Command { 196 | number: 0, 197 | action: Action::Instruction(Instruction::None), 198 | object: None, 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/m/editor.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::sync::mpsc::channel; 3 | use std::sync::mpsc::{Receiver, Sender}; 4 | use std::sync::{Arc, Mutex}; 5 | 6 | use rustbox::{Event, RustBox}; 7 | 8 | use bindings; 9 | use buffer::Buffer; 10 | use command::Command; 11 | use command::{Action, Instruction, Operation}; 12 | use input::Input; 13 | use key::Key; 14 | use view::View; 15 | 16 | /// The main Editor structure 17 | /// 18 | /// This is the top-most structure in Iota. 19 | pub struct Editor { 20 | view: View, 21 | running: bool, 22 | rb: RustBox, 23 | 24 | command_queue: Receiver, 25 | command_sender: Sender, 26 | } 27 | 28 | impl Editor { 29 | /// Create a new Editor instance from the given source 30 | pub fn new(source: Input, rb: RustBox) -> Editor { 31 | let height = rb.height(); 32 | let width = rb.width(); 33 | 34 | let (snd, recv) = channel(); 35 | 36 | // if source is File, then read from file, it none, then new a buffer 37 | let buffer = match source { 38 | Input::Filename(path) => match path { 39 | Some(path) => Buffer::from(PathBuf::from(path)), 40 | None => Buffer::new(), 41 | }, 42 | Input::Stdin(reader) => Buffer::from(reader), 43 | }; 44 | 45 | let view = View::new(Arc::new(Mutex::new(buffer)), width, height); 46 | 47 | Editor { 48 | view: view, 49 | running: true, 50 | rb: rb, 51 | 52 | command_queue: recv, 53 | command_sender: snd, 54 | } 55 | } 56 | 57 | /// Handle key events 58 | /// 59 | /// Key events can be handled in an Overlay, OR in the current Mode. 60 | /// 61 | /// If there is an active Overlay, the key event is sent there, which gives 62 | /// back an OverlayEvent. We then parse this OverlayEvent and determine if 63 | /// the Overlay is finished and can be cleared. The response from the 64 | /// Overlay is then converted to a Command and sent off to be handled. 65 | /// 66 | /// If there is no active Overlay, the key event is sent to the current 67 | /// Mode, which returns a Command which we dispatch to handle_command. 68 | fn handle_key_event(&mut self, event: Event) { 69 | let key = Key::from_event(&mut self.rb, event); 70 | 71 | let key = match key { 72 | Some(k) => k, 73 | None => return, 74 | }; 75 | 76 | let command = bindings::handle_key_event(key); 77 | 78 | self.view.clear(&mut self.rb); 79 | let _ = self.command_sender.send(command); 80 | } 81 | 82 | /// Handle resize events 83 | /// 84 | /// width and height represent the new height of the window. 85 | fn handle_resize_event(&mut self, width: usize, height: usize) { 86 | self.view.resize(width, height); 87 | } 88 | 89 | /// Draw the current view to the frontend 90 | fn draw(&mut self) { 91 | self.view.draw(&mut self.rb); 92 | } 93 | 94 | /// Handle the given command, performing the associated action 95 | fn handle_command(&mut self, command: Command) { 96 | let repeat = if command.number > 0 { 97 | command.number 98 | } else { 99 | 1 100 | }; 101 | for _ in 0..repeat { 102 | match command.action { 103 | Action::Instruction(i) => self.handle_instruction(i, command), 104 | Action::Operation(o) => self.handle_operation(o, command), 105 | } 106 | } 107 | } 108 | 109 | fn handle_instruction(&mut self, instruction: Instruction, command: Command) { 110 | match instruction { 111 | Instruction::SaveBuffer => { 112 | self.view.try_save_buffer(); 113 | } 114 | Instruction::ExitEditor => { 115 | if self.view.buffer_is_dirty() { 116 | let _ = self.command_sender 117 | .send(Command::show_message("↝ Unsaved changes")); 118 | } else { 119 | self.running = false; 120 | } 121 | } 122 | Instruction::ForceExitEditor => { 123 | // exit directly, ignore the buffer 124 | self.running = false; 125 | } 126 | Instruction::SetMark(mark) => { 127 | if let Some(object) = command.object { 128 | self.view.move_mark(mark, object) 129 | } 130 | } 131 | Instruction::ShowMessage(msg) => { 132 | // let n_msg = "00".to_string(); 133 | // let n_msg = n_msg + msg; 134 | // let n_msg_static:&str = n_msg.as_str(); 135 | self.view.show_message(msg); 136 | }, 137 | 138 | _ => {} 139 | } 140 | } 141 | 142 | fn handle_operation(&mut self, operation: Operation, command: Command) { 143 | match operation { 144 | Operation::Insert(c) => { 145 | for _ in 0..command.number { 146 | self.view.insert_char(c) 147 | } 148 | } 149 | Operation::Paste => { 150 | self.view.paste(); 151 | } 152 | Operation::DeleteSelection => { 153 | self.view.delete_selection(); 154 | } 155 | Operation::DuplicateSelection => { 156 | self.view.duplicate_selection(); 157 | } 158 | Operation::CutSelection => { 159 | self.view.cut_selection(); 160 | } 161 | Operation::CopySelection => { 162 | self.view.copy_selection(); 163 | } 164 | Operation::MoveSelection(down) => { 165 | self.view.move_selection(down); 166 | } 167 | Operation::DeleteFromMark(m) => { 168 | if command.object.is_some() { 169 | self.view 170 | .delete_from_mark_to_object(m, command.object.unwrap()) 171 | } 172 | } 173 | Operation::Undo => self.view.undo(), 174 | Operation::Redo => self.view.redo(), 175 | } 176 | } 177 | 178 | /// Start Iota! 179 | pub fn start(&mut self) { 180 | while self.running { 181 | self.draw(); 182 | self.rb.present(); 183 | self.view.maybe_clear_message(); 184 | 185 | match self.rb.poll_event(true) { 186 | Ok(Event::ResizeEvent(width, height)) => { 187 | self.handle_resize_event(width as usize, height as usize) 188 | } 189 | Ok(key_event) => self.handle_key_event(key_event), 190 | _ => {} 191 | } 192 | 193 | while let Ok(message) = self.command_queue.try_recv() { 194 | self.handle_command(message) 195 | } 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/m/input.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | /// A source of Input for the Editor. 4 | /// 5 | /// This is used at startup, where the user can either open a file, or 6 | /// start Iota with data from stdin. 7 | pub enum Input { 8 | /// A Filename 9 | Filename(Option), 10 | 11 | /// The stdin reader 12 | Stdin(io::Stdin), 13 | } 14 | -------------------------------------------------------------------------------- /src/m/iterators.rs: -------------------------------------------------------------------------------- 1 | use gapbuffer::GapBuffer; 2 | 3 | pub struct Lines<'a> { 4 | pub buffer: &'a GapBuffer, 5 | pub tail: usize, 6 | pub head: usize, 7 | } 8 | 9 | impl<'a> Iterator for Lines<'a> { 10 | type Item = String; 11 | 12 | fn next(&mut self) -> Option { 13 | if self.tail == self.head { 14 | return None; 15 | } 16 | let old_tail = self.tail; 17 | //update tail to either the first char after the next \n or to self.head 18 | self.tail = (old_tail..self.head) 19 | .filter(|i| *i + 1 == self.head || self.buffer[*i] == '\n') 20 | .take(1) 21 | .next() 22 | .unwrap() + 1; 23 | Some( 24 | (old_tail..if self.tail == self.head { 25 | self.tail - 1 26 | } else { 27 | self.tail 28 | }).map(|i| self.buffer[i]) 29 | .collect(), 30 | ) 31 | } 32 | 33 | fn size_hint(&self) -> (usize, Option) { 34 | //TODO: this is technically correct but a better estimate could be implemented 35 | (1, Some(self.head)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/m/key.rs: -------------------------------------------------------------------------------- 1 | use std::char; 2 | use std::time::Duration; 3 | 4 | use rustbox::{Event, RustBox}; 5 | 6 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 7 | pub enum Key { 8 | Tab, 9 | Enter, 10 | Esc, 11 | Backspace, 12 | Right, 13 | Left, 14 | Down, 15 | Up, 16 | Delete, 17 | Home, 18 | End, 19 | PgUp, 20 | PgDown, 21 | CtrlLeft, 22 | CtrlRight, 23 | CtrlUp, 24 | CtrlDown, 25 | 26 | Char(char), 27 | Ctrl(char), 28 | } 29 | 30 | impl Key { 31 | pub fn from_special_code(code: u16) -> Option { 32 | match code { 33 | 1 => Some(Key::Ctrl('a')), 34 | 2 => Some(Key::Ctrl('b')), 35 | 3 => Some(Key::Ctrl('c')), 36 | 4 => Some(Key::Ctrl('d')), 37 | 5 => Some(Key::Ctrl('e')), 38 | 6 => Some(Key::Ctrl('f')), 39 | 7 => Some(Key::Ctrl('g')), 40 | 8 => Some(Key::Backspace), 41 | 9 => Some(Key::Tab), 42 | 10 => Some(Key::Ctrl('j')), 43 | 11 => Some(Key::Ctrl('k')), 44 | 12 => Some(Key::Ctrl('l')), 45 | 13 => Some(Key::Enter), 46 | 14 => Some(Key::Ctrl('n')), 47 | 15 => Some(Key::Ctrl('o')), 48 | 16 => Some(Key::Ctrl('p')), 49 | 17 => Some(Key::Ctrl('q')), 50 | 18 => Some(Key::Ctrl('r')), 51 | 19 => Some(Key::Ctrl('s')), 52 | 20 => Some(Key::Ctrl('t')), 53 | 21 => Some(Key::Ctrl('u')), 54 | 22 => Some(Key::Ctrl('v')), 55 | 23 => Some(Key::Ctrl('w')), 56 | 24 => Some(Key::Ctrl('x')), 57 | 25 => Some(Key::Ctrl('y')), 58 | 26 => Some(Key::Ctrl('z')), 59 | 27 => Some(Key::Esc), 60 | 32 => Some(Key::Char(' ')), 61 | 127 => Some(Key::Backspace), 62 | 65514 => Some(Key::Right), 63 | 65515 => Some(Key::Left), 64 | 65516 => Some(Key::Down), 65 | 65517 => Some(Key::Up), 66 | 65518 => Some(Key::PgDown), 67 | 65519 => Some(Key::PgUp), 68 | 65520 => Some(Key::End), 69 | 65521 => Some(Key::Home), 70 | 65522 => Some(Key::Delete), 71 | _ => None, 72 | } 73 | } 74 | 75 | pub fn from_chord(rb: &mut RustBox, start: u16) -> Option { 76 | let chord = Key::get_chord(rb, start); 77 | 78 | match chord.as_str() { 79 | "\x1b[1;5A" => Some(Key::CtrlUp), 80 | "\x1b[1;5B" => Some(Key::CtrlDown), 81 | "\x1b[1;5C" => Some(Key::CtrlRight), 82 | "\x1b[1;5D" => Some(Key::CtrlLeft), 83 | _ => Key::from_special_code(start), 84 | } 85 | } 86 | 87 | pub fn get_chord(rb: &mut RustBox, start: u16) -> String { 88 | // Copy any data waiting to a string 89 | // There may be a cleaner way to do this? 90 | let mut chord = char::from_u32(u32::from(start)).unwrap().to_string(); 91 | while let Ok(Event::KeyEventRaw(_, _, ch)) = rb.peek_event(Duration::from_secs(0), true) { 92 | chord.push(char::from_u32(ch).unwrap()) 93 | } 94 | 95 | chord 96 | } 97 | 98 | pub fn from_event(rb: &mut RustBox, event: Event) -> Option { 99 | match event { 100 | Event::KeyEventRaw(_, k, ch) => match k { 101 | 0 => char::from_u32(ch).map(Key::Char), 102 | 0x1b => Key::from_chord(rb, 0x1b), 103 | a => Key::from_special_code(a), 104 | }, 105 | _ => None, 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/m/lib.rs: -------------------------------------------------------------------------------- 1 | //! Iota 2 | //! 3 | //! A highly customisable text editor built with modern hardware in mind. 4 | //! 5 | //! This module contains all you need to create an `iota` executable. 6 | 7 | #![cfg_attr(feature = "clippy", feature(plugin))] 8 | #![cfg_attr(feature = "clippy", plugin(clippy))] 9 | #![warn(missing_docs)] 10 | 11 | extern crate gapbuffer; 12 | extern crate regex; 13 | extern crate rustbox; 14 | extern crate unicode_width; 15 | #[macro_use] 16 | extern crate lazy_static; 17 | 18 | pub use editor::Editor; 19 | pub use input::Input; 20 | 21 | mod bindings; 22 | mod buffer; 23 | mod command; 24 | mod editor; 25 | mod input; 26 | mod iterators; 27 | mod key; 28 | mod log; 29 | mod textobject; 30 | mod utils; 31 | mod view; 32 | -------------------------------------------------------------------------------- /src/m/log.rs: -------------------------------------------------------------------------------- 1 | //! Primitive command log, currently used for undo / redo. 2 | //! This is a deliberately unoptimized representation, for simplicity. It is by no means final. 3 | 4 | use std::mem; 5 | 6 | /// Represents a modification of data. 7 | pub enum Change { 8 | ///Character insertion. 9 | Insert(usize, char), 10 | ///Character removal. 11 | Remove(usize, char), 12 | } 13 | 14 | impl Change { 15 | /// Reverses a change, consuming it in the process 16 | pub fn reverse(self) -> Change { 17 | match self { 18 | Change::Insert(usize, char) => Change::Remove(usize, char), 19 | Change::Remove(usize, char) => Change::Insert(usize, char), 20 | } 21 | } 22 | } 23 | 24 | /// Log entry 25 | /// Entries may only be played linearly--they don't make sense out of order. 26 | pub struct LogEntry { 27 | /// The initial point position associated with this log entry. 28 | /// 29 | /// The OLD point position. 30 | init_point: usize, 31 | /// The NEW point position. 32 | pub end_point: usize, 33 | /// The changes associated with this log entry, in order of occurence (an undo will replay 34 | /// their inverses, backwards). 35 | pub changes: Vec, 36 | } 37 | 38 | impl LogEntry { 39 | /// Reverse a log entry, consuming it in the process. 40 | pub fn reverse(mut self) -> LogEntry { 41 | self.changes.reverse(); 42 | LogEntry { 43 | init_point: self.end_point, 44 | end_point: self.init_point, 45 | changes: self.changes 46 | .into_iter() 47 | .map(|change| change.reverse()) 48 | .collect(), 49 | } 50 | } 51 | } 52 | 53 | /// A set of `Change`s that should be treated atomically. 54 | /// 55 | /// This transaction always has an associated entry log. When the transaction is dropped, the 56 | /// entries are committed. 57 | pub struct Transaction<'a> { 58 | /// Currently, only one transaction may be open at a time. 59 | entries: &'a mut Log, 60 | /// The LogEntry under construction by the transaction. Every data modification should be 61 | /// recorded with the open Transaction. 62 | entry: LogEntry, 63 | } 64 | 65 | impl<'a> Transaction<'a> { 66 | /// Log a change with this transaction. 67 | /// 68 | /// The logging should occur after the change has been executed. This may eventually allow 69 | /// rollback in case of failure. 70 | pub fn log(&mut self, change: Change, idx: usize) { 71 | self.entry.changes.push(change); 72 | self.entry.end_point = idx; 73 | } 74 | } 75 | 76 | impl<'a> Drop for Transaction<'a> { 77 | fn drop(&mut self) { 78 | // Check to see if there were any changes, and if not return early. 79 | if self.entry.changes.is_empty() { 80 | return; 81 | } 82 | // Create the new log entry 83 | let entry = LogEntry { 84 | changes: mem::replace(&mut self.entry.changes, Vec::new()), 85 | ..self.entry 86 | }; 87 | // Commit the transaction. 88 | self.entries.undo.push(entry); 89 | // Clear the redo entries now that the transaction has been committed. 90 | self.entries.redo.clear(); 91 | } 92 | } 93 | 94 | /// Log entries structure. Just two stacks. 95 | pub struct Log { 96 | /// Undo log entries--LIFO stack. 97 | undo: Vec, 98 | /// Redo log entries--LIFO stack. Cleared after a new change (other than an undo or redo) 99 | /// is committed. 100 | redo: Vec, 101 | } 102 | 103 | impl Log { 104 | /// Set up log entries. They are initially empty. 105 | pub fn new() -> Log { 106 | Log { 107 | undo: Vec::new(), 108 | redo: Vec::new(), 109 | } 110 | } 111 | 112 | /// Start a new transaction. 113 | /// 114 | /// This returns a RAII guard that can be used to record edits during the transaction. 115 | #[cfg_attr(feature = "clippy", allow(needless_lifetimes))] 116 | pub fn start(&mut self, idx: usize) -> Transaction { 117 | Transaction { 118 | entries: self, 119 | entry: LogEntry { 120 | init_point: idx, 121 | end_point: idx, 122 | changes: Vec::new(), 123 | }, 124 | } 125 | } 126 | 127 | /// This reverses the most recent change on the undo stack, places the new change on the redo 128 | /// stack, and then returns a reference to it. It is the caller's responsibility to actually 129 | /// perform the change. 130 | pub fn undo(&mut self) -> Option<&LogEntry> { 131 | match self.undo.pop() { 132 | Some(change) => { 133 | let last = self.redo.len(); 134 | self.redo.push(change.reverse()); 135 | Some(&self.redo[last]) 136 | } 137 | None => None, 138 | } 139 | } 140 | /// This reverses the most recent change on the redo stack, places the new change on the undo 141 | /// stack, and then returns a reference to it. It is the caller's responsibility to actually 142 | /// perform the change. 143 | pub fn redo(&mut self) -> Option<&LogEntry> { 144 | match self.redo.pop() { 145 | Some(change) => { 146 | let last = self.undo.len(); 147 | self.undo.push(change.reverse()); 148 | Some(&self.undo[last]) 149 | } 150 | None => None, 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/m/textobject.rs: -------------------------------------------------------------------------------- 1 | use std::default::Default; 2 | 3 | use buffer::Mark; 4 | 5 | #[derive(Copy, Clone, Debug)] 6 | pub enum Kind { 7 | Char, 8 | Line(Anchor), 9 | Word(Anchor), 10 | Selection(Anchor), 11 | } 12 | 13 | impl Default for Kind { 14 | fn default() -> Kind { 15 | Kind::Char 16 | } 17 | } 18 | 19 | #[derive(Copy, Clone, Debug)] 20 | pub enum Anchor { 21 | Start, // First index within TextObject 22 | End, // Last index within TextObject 23 | Same, // Same as index within current TextObject of the same Kind 24 | } 25 | 26 | impl Default for Anchor { 27 | fn default() -> Anchor { 28 | Anchor::Same 29 | } 30 | } 31 | 32 | #[derive(Copy, Clone, Debug)] 33 | pub enum Offset { 34 | Absolute(usize), 35 | Backward(usize, Mark), 36 | Forward(usize, Mark), 37 | } 38 | 39 | impl Default for Offset { 40 | fn default() -> Offset { 41 | Offset::Forward(0, Mark::Cursor(0)) 42 | } 43 | } 44 | 45 | #[derive(Copy, Clone, Debug)] 46 | pub struct TextObject { 47 | pub kind: Kind, 48 | pub offset: Offset, 49 | } 50 | 51 | impl Default for TextObject { 52 | fn default() -> TextObject { 53 | TextObject { 54 | kind: Default::default(), 55 | offset: Default::default(), 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/m/utils.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use regex::Regex; 4 | 5 | static ANSI_COLORS: [[i32; 3]; 256] = [ 6 | [0x00, 0x00, 0x00], 7 | [0x80, 0x00, 0x00], 8 | [0x00, 0x80, 0x00], 9 | [0x80, 0x80, 0x00], 10 | [0x00, 0x00, 0x80], 11 | [0x80, 0x00, 0x80], 12 | [0x00, 0x80, 0x80], 13 | [0xc0, 0xc0, 0xc0], 14 | [0x80, 0x80, 0x80], 15 | [0xff, 0x00, 0x00], 16 | [0x00, 0xff, 0x00], 17 | [0xff, 0xff, 0x00], 18 | [0x00, 0x00, 0xff], 19 | [0xff, 0x00, 0xff], 20 | [0x00, 0xff, 0xff], 21 | [0xff, 0xff, 0xff], 22 | [0x00, 0x00, 0x00], 23 | [0x00, 0x00, 0x5f], 24 | [0x00, 0x00, 0x87], 25 | [0x00, 0x00, 0xaf], 26 | [0x00, 0x00, 0xd7], 27 | [0x00, 0x00, 0xff], 28 | [0x00, 0x5f, 0x00], 29 | [0x00, 0x5f, 0x5f], 30 | [0x00, 0x5f, 0x87], 31 | [0x00, 0x5f, 0xaf], 32 | [0x00, 0x5f, 0xd7], 33 | [0x00, 0x5f, 0xff], 34 | [0x00, 0x87, 0x00], 35 | [0x00, 0x87, 0x5f], 36 | [0x00, 0x87, 0x87], 37 | [0x00, 0x87, 0xaf], 38 | [0x00, 0x87, 0xd7], 39 | [0x00, 0x87, 0xff], 40 | [0x00, 0xaf, 0x00], 41 | [0x00, 0xaf, 0x5f], 42 | [0x00, 0xaf, 0x87], 43 | [0x00, 0xaf, 0xaf], 44 | [0x00, 0xaf, 0xd7], 45 | [0x00, 0xaf, 0xff], 46 | [0x00, 0xd7, 0x00], 47 | [0x00, 0xd7, 0x5f], 48 | [0x00, 0xd7, 0x87], 49 | [0x00, 0xd7, 0xaf], 50 | [0x00, 0xd7, 0xd7], 51 | [0x00, 0xd7, 0xff], 52 | [0x00, 0xff, 0x00], 53 | [0x00, 0xff, 0x5f], 54 | [0x00, 0xff, 0x87], 55 | [0x00, 0xff, 0xaf], 56 | [0x00, 0xff, 0xd7], 57 | [0x00, 0xff, 0xff], 58 | [0x5f, 0x00, 0x00], 59 | [0x5f, 0x00, 0x5f], 60 | [0x5f, 0x00, 0x87], 61 | [0x5f, 0x00, 0xaf], 62 | [0x5f, 0x00, 0xd7], 63 | [0x5f, 0x00, 0xff], 64 | [0x5f, 0x5f, 0x00], 65 | [0x5f, 0x5f, 0x5f], 66 | [0x5f, 0x5f, 0x87], 67 | [0x5f, 0x5f, 0xaf], 68 | [0x5f, 0x5f, 0xd7], 69 | [0x5f, 0x5f, 0xff], 70 | [0x5f, 0x87, 0x00], 71 | [0x5f, 0x87, 0x5f], 72 | [0x5f, 0x87, 0x87], 73 | [0x5f, 0x87, 0xaf], 74 | [0x5f, 0x87, 0xd7], 75 | [0x5f, 0x87, 0xff], 76 | [0x5f, 0xaf, 0x00], 77 | [0x5f, 0xaf, 0x5f], 78 | [0x5f, 0xaf, 0x87], 79 | [0x5f, 0xaf, 0xaf], 80 | [0x5f, 0xaf, 0xd7], 81 | [0x5f, 0xaf, 0xff], 82 | [0x5f, 0xd7, 0x00], 83 | [0x5f, 0xd7, 0x5f], 84 | [0x5f, 0xd7, 0x87], 85 | [0x5f, 0xd7, 0xaf], 86 | [0x5f, 0xd7, 0xd7], 87 | [0x5f, 0xd7, 0xff], 88 | [0x5f, 0xff, 0x00], 89 | [0x5f, 0xff, 0x5f], 90 | [0x5f, 0xff, 0x87], 91 | [0x5f, 0xff, 0xaf], 92 | [0x5f, 0xff, 0xd7], 93 | [0x5f, 0xff, 0xff], 94 | [0x87, 0x00, 0x00], 95 | [0x87, 0x00, 0x5f], 96 | [0x87, 0x00, 0x87], 97 | [0x87, 0x00, 0xaf], 98 | [0x87, 0x00, 0xd7], 99 | [0x87, 0x00, 0xff], 100 | [0x87, 0x5f, 0x00], 101 | [0x87, 0x5f, 0x5f], 102 | [0x87, 0x5f, 0x87], 103 | [0x87, 0x5f, 0xaf], 104 | [0x87, 0x5f, 0xd7], 105 | [0x87, 0x5f, 0xff], 106 | [0x87, 0x87, 0x00], 107 | [0x87, 0x87, 0x5f], 108 | [0x87, 0x87, 0x87], 109 | [0x87, 0x87, 0xaf], 110 | [0x87, 0x87, 0xd7], 111 | [0x87, 0x87, 0xff], 112 | [0x87, 0xaf, 0x00], 113 | [0x87, 0xaf, 0x5f], 114 | [0x87, 0xaf, 0x87], 115 | [0x87, 0xaf, 0xaf], 116 | [0x87, 0xaf, 0xd7], 117 | [0x87, 0xaf, 0xff], 118 | [0x87, 0xd7, 0x00], 119 | [0x87, 0xd7, 0x5f], 120 | [0x87, 0xd7, 0x87], 121 | [0x87, 0xd7, 0xaf], 122 | [0x87, 0xd7, 0xd7], 123 | [0x87, 0xd7, 0xff], 124 | [0x87, 0xff, 0x00], 125 | [0x87, 0xff, 0x5f], 126 | [0x87, 0xff, 0x87], 127 | [0x87, 0xff, 0xaf], 128 | [0x87, 0xff, 0xd7], 129 | [0x87, 0xff, 0xff], 130 | [0xaf, 0x00, 0x00], 131 | [0xaf, 0x00, 0x5f], 132 | [0xaf, 0x00, 0x87], 133 | [0xaf, 0x00, 0xaf], 134 | [0xaf, 0x00, 0xd7], 135 | [0xaf, 0x00, 0xff], 136 | [0xaf, 0x5f, 0x00], 137 | [0xaf, 0x5f, 0x5f], 138 | [0xaf, 0x5f, 0x87], 139 | [0xaf, 0x5f, 0xaf], 140 | [0xaf, 0x5f, 0xd7], 141 | [0xaf, 0x5f, 0xff], 142 | [0xaf, 0x87, 0x00], 143 | [0xaf, 0x87, 0x5f], 144 | [0xaf, 0x87, 0x87], 145 | [0xaf, 0x87, 0xaf], 146 | [0xaf, 0x87, 0xd7], 147 | [0xaf, 0x87, 0xff], 148 | [0xaf, 0xaf, 0x00], 149 | [0xaf, 0xaf, 0x5f], 150 | [0xaf, 0xaf, 0x87], 151 | [0xaf, 0xaf, 0xaf], 152 | [0xaf, 0xaf, 0xd7], 153 | [0xaf, 0xaf, 0xff], 154 | [0xaf, 0xd7, 0x00], 155 | [0xaf, 0xd7, 0x5f], 156 | [0xaf, 0xd7, 0x87], 157 | [0xaf, 0xd7, 0xaf], 158 | [0xaf, 0xd7, 0xd7], 159 | [0xaf, 0xd7, 0xff], 160 | [0xaf, 0xff, 0x00], 161 | [0xaf, 0xff, 0x5f], 162 | [0xaf, 0xff, 0x87], 163 | [0xaf, 0xff, 0xaf], 164 | [0xaf, 0xff, 0xd7], 165 | [0xaf, 0xff, 0xff], 166 | [0xd7, 0x00, 0x00], 167 | [0xd7, 0x00, 0x5f], 168 | [0xd7, 0x00, 0x87], 169 | [0xd7, 0x00, 0xaf], 170 | [0xd7, 0x00, 0xd7], 171 | [0xd7, 0x00, 0xff], 172 | [0xd7, 0x5f, 0x00], 173 | [0xd7, 0x5f, 0x5f], 174 | [0xd7, 0x5f, 0x87], 175 | [0xd7, 0x5f, 0xaf], 176 | [0xd7, 0x5f, 0xd7], 177 | [0xd7, 0x5f, 0xff], 178 | [0xd7, 0x87, 0x00], 179 | [0xd7, 0x87, 0x5f], 180 | [0xd7, 0x87, 0x87], 181 | [0xd7, 0x87, 0xaf], 182 | [0xd7, 0x87, 0xd7], 183 | [0xd7, 0x87, 0xff], 184 | [0xd7, 0xaf, 0x00], 185 | [0xd7, 0xaf, 0x5f], 186 | [0xd7, 0xaf, 0x87], 187 | [0xd7, 0xaf, 0xaf], 188 | [0xd7, 0xaf, 0xd7], 189 | [0xd7, 0xaf, 0xff], 190 | [0xd7, 0xd7, 0x00], 191 | [0xd7, 0xd7, 0x5f], 192 | [0xd7, 0xd7, 0x87], 193 | [0xd7, 0xd7, 0xaf], 194 | [0xd7, 0xd7, 0xd7], 195 | [0xd7, 0xd7, 0xff], 196 | [0xd7, 0xff, 0x00], 197 | [0xd7, 0xff, 0x5f], 198 | [0xd7, 0xff, 0x87], 199 | [0xd7, 0xff, 0xaf], 200 | [0xd7, 0xff, 0xd7], 201 | [0xd7, 0xff, 0xff], 202 | [0xff, 0x00, 0x00], 203 | [0xff, 0x00, 0x5f], 204 | [0xff, 0x00, 0x87], 205 | [0xff, 0x00, 0xaf], 206 | [0xff, 0x00, 0xd7], 207 | [0xff, 0x00, 0xff], 208 | [0xff, 0x5f, 0x00], 209 | [0xff, 0x5f, 0x5f], 210 | [0xff, 0x5f, 0x87], 211 | [0xff, 0x5f, 0xaf], 212 | [0xff, 0x5f, 0xd7], 213 | [0xff, 0x5f, 0xff], 214 | [0xff, 0x87, 0x00], 215 | [0xff, 0x87, 0x5f], 216 | [0xff, 0x87, 0x87], 217 | [0xff, 0x87, 0xaf], 218 | [0xff, 0x87, 0xd7], 219 | [0xff, 0x87, 0xff], 220 | [0xff, 0xaf, 0x00], 221 | [0xff, 0xaf, 0x5f], 222 | [0xff, 0xaf, 0x87], 223 | [0xff, 0xaf, 0xaf], 224 | [0xff, 0xaf, 0xd7], 225 | [0xff, 0xaf, 0xff], 226 | [0xff, 0xd7, 0x00], 227 | [0xff, 0xd7, 0x5f], 228 | [0xff, 0xd7, 0x87], 229 | [0xff, 0xd7, 0xaf], 230 | [0xff, 0xd7, 0xd7], 231 | [0xff, 0xd7, 0xff], 232 | [0xff, 0xff, 0x00], 233 | [0xff, 0xff, 0x5f], 234 | [0xff, 0xff, 0x87], 235 | [0xff, 0xff, 0xaf], 236 | [0xff, 0xff, 0xd7], 237 | [0xff, 0xff, 0xff], 238 | [0x08, 0x08, 0x08], 239 | [0x12, 0x12, 0x12], 240 | [0x1c, 0x1c, 0x1c], 241 | [0x26, 0x26, 0x26], 242 | [0x30, 0x30, 0x30], 243 | [0x3a, 0x3a, 0x3a], 244 | [0x44, 0x44, 0x44], 245 | [0x4e, 0x4e, 0x4e], 246 | [0x58, 0x58, 0x58], 247 | [0x60, 0x60, 0x60], 248 | [0x66, 0x66, 0x66], 249 | [0x76, 0x76, 0x76], 250 | [0x80, 0x80, 0x80], 251 | [0x8a, 0x8a, 0x8a], 252 | [0x94, 0x94, 0x94], 253 | [0x9e, 0x9e, 0x9e], 254 | [0xa8, 0xa8, 0xa8], 255 | [0xb2, 0xb2, 0xb2], 256 | [0xbc, 0xbc, 0xbc], 257 | [0xc6, 0xc6, 0xc6], 258 | [0xd0, 0xd0, 0xd0], 259 | [0xda, 0xda, 0xda], 260 | [0xe4, 0xe4, 0xe4], 261 | [0xee, 0xee, 0xee], 262 | ]; 263 | 264 | lazy_static! { 265 | static ref RE: Regex = Regex::new("(..)(..)(..)").unwrap(); 266 | } 267 | 268 | #[cfg_attr(feature = "clippy", allow(needless_range_loop))] 269 | pub fn rgb_to_short(rgb: &str) -> usize { 270 | let matches = RE.captures(rgb).unwrap(); 271 | let parts = vec![ 272 | u8::from_str_radix(matches.at(1).unwrap(), 16).unwrap(), 273 | u8::from_str_radix(matches.at(2).unwrap(), 16).unwrap(), 274 | u8::from_str_radix(matches.at(3).unwrap(), 16).unwrap(), 275 | ]; 276 | 277 | let mut best = 0; 278 | let mut best_distance = 255 * 255 * 3 + 1; 279 | for i in 16..255 { 280 | let ansi_color = ANSI_COLORS[i]; 281 | let dr = ansi_color[0] - i32::from(parts[0]); 282 | let dg = ansi_color[1] - i32::from(parts[1]); 283 | let db = ansi_color[2] - i32::from(parts[2]); 284 | let distance = dr * dr + dg * dg + db * db; 285 | 286 | if distance < best_distance { 287 | best_distance = distance; 288 | best = i as usize; 289 | } 290 | } 291 | 292 | best 293 | } 294 | 295 | pub fn char_width(c: char, is_cjk: bool, tab_width: usize, position: usize) -> Option { 296 | use unicode_width::UnicodeWidthChar; 297 | 298 | if c == '\t' { 299 | Some(tab_width - position % tab_width) 300 | } else if c == '\n' { 301 | Some(1) 302 | } else if is_cjk { 303 | UnicodeWidthChar::width_cjk(c) 304 | } else { 305 | UnicodeWidthChar::width(c) 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/m/view.rs: -------------------------------------------------------------------------------- 1 | use rustbox::{Color, RustBox, Style as RustBoxStyle}; 2 | use std::borrow::Cow; 3 | use std::cmp; 4 | use std::fs::{rename, File}; 5 | use std::io::Write; 6 | use std::path::PathBuf; 7 | use std::sync::{Arc, Mutex}; 8 | use std::time::SystemTime; 9 | use std::path::Path; 10 | 11 | 12 | extern crate clipboard; 13 | 14 | use self::clipboard::{ClipboardContext, ClipboardProvider}; 15 | 16 | use unicode_width::UnicodeWidthChar; 17 | 18 | use buffer::{Buffer, Mark}; 19 | use textobject::{Anchor, Kind, Offset, TextObject}; 20 | 21 | /// A View is an abstract Window (into a Buffer). 22 | /// 23 | /// It draws a portion of a Buffer to a `UIBuffer` which in turn is drawn to the 24 | /// screen. It maintains the status bar for the current view, the "dirty status" 25 | /// which is whether the buffer has been modified or not and a number of other 26 | /// pieces of information. 27 | pub struct View { 28 | pub buffer: Arc>, 29 | 30 | /// Used to store clipboard if system clipboard is not available 31 | clipboard: String, 32 | 33 | height: usize, 34 | width: usize, 35 | 36 | /// First character of the top line to be displayed 37 | top_line: Mark, 38 | 39 | /// Index into the top_line - used for horizontal scrolling 40 | left_col: usize, 41 | 42 | /// The current View's cursor - a reference into the Buffer 43 | cursor: Mark, 44 | 45 | /// Number of lines from the top/bottom of the View after which vertical 46 | /// scrolling begins. 47 | threshold: usize, 48 | 49 | /// Message to be displayed in the status bar along with the time it 50 | /// was displayed. 51 | message: Option<(&'static str, SystemTime)>, 52 | } 53 | 54 | impl View { 55 | pub fn new(buffer: Arc>, width: usize, height: usize) -> View { 56 | let cursor = Mark::Cursor(0); 57 | let top_line = Mark::DisplayMark(0); 58 | 59 | { 60 | let mut b = buffer.lock().unwrap(); 61 | 62 | b.set_mark(cursor, 0); 63 | b.set_mark(top_line, 0); 64 | } 65 | 66 | View { 67 | buffer: buffer, 68 | top_line: top_line, 69 | left_col: 0, 70 | cursor: cursor, 71 | threshold: 5, 72 | message: None, 73 | height: height, 74 | width: width, 75 | clipboard: String::from(""), 76 | } 77 | } 78 | 79 | pub fn selection_start() -> TextObject { 80 | TextObject { 81 | kind: Kind::Line(Anchor::Start), 82 | offset: Offset::Backward(0, Mark::Cursor(0)), 83 | } 84 | } 85 | pub fn selection_end() -> TextObject { 86 | TextObject { 87 | kind: Kind::Line(Anchor::Same), //Anchor::Start makes more sense, but isn't implemented 88 | offset: Offset::Forward(1, Mark::Cursor(0)), 89 | } 90 | } 91 | 92 | /// Get the height of the View. 93 | /// 94 | /// This is the height of the UIBuffer minus the status bar height. 95 | pub fn get_height(&self) -> usize { 96 | self.height - 1 97 | } 98 | 99 | /// Get the width of the View. 100 | pub fn get_width(&self) -> usize { 101 | self.width 102 | } 103 | 104 | /// Resize the view 105 | /// 106 | /// This involves simply changing the size of the associated UIBuffer 107 | pub fn resize(&mut self, width: usize, height: usize) { 108 | self.height = height; 109 | self.width = width; 110 | } 111 | 112 | /// Clear the buffer 113 | /// 114 | /// Fills every cell in the UIBuffer with the space (' ') char. 115 | pub fn clear(&mut self, rb: &mut RustBox) { 116 | for row in 0..self.height { 117 | for col in 0..self.width { 118 | rb.print_char( 119 | col, 120 | row, 121 | RustBoxStyle::empty(), 122 | Color::White, 123 | Color::Black, 124 | ' ', 125 | ); 126 | } 127 | } 128 | } 129 | 130 | pub fn draw(&mut self, rb: &mut RustBox) { 131 | self.clear(rb); 132 | { 133 | let buffer = self.buffer.lock().unwrap(); 134 | let height = self.get_height() - 1; 135 | 136 | // FIXME: don't use unwrap here 137 | // This will fail if for some reason the buffer doesnt have 138 | // the top_line mark 139 | let mut lines = buffer.lines_from(self.top_line).unwrap().take(height); 140 | for y_position in 0..height { 141 | let line = lines.next(); 142 | draw_line( 143 | rb, 144 | line.unwrap_or(String::from("")), 145 | y_position, 146 | self.left_col, 147 | ); 148 | } 149 | } 150 | 151 | self.draw_status(rb); 152 | self.draw_cursor(rb); 153 | } 154 | 155 | #[cfg_attr(feature = "clippy", allow(needless_range_loop))] 156 | fn draw_status(&mut self, rb: &mut RustBox) { 157 | let buffer = self.buffer.lock().unwrap(); 158 | let buffer_status = buffer.status_text(); 159 | let mut cursor_status = buffer 160 | .get_mark_display_coords(self.cursor) 161 | .unwrap_or((0, 0)); 162 | cursor_status = (cursor_status.0 + 1, cursor_status.1 + 1); 163 | let status_text = format!( 164 | "{} ({}, {})", 165 | buffer_status, cursor_status.0, cursor_status.1 166 | ).into_bytes(); 167 | let status_text_len = status_text.len(); 168 | let width = self.get_width(); 169 | let height = self.get_height() - 1; 170 | 171 | for index in 0..width { 172 | let ch: char = if index < status_text_len { 173 | status_text[index] as char 174 | } else { 175 | ' ' 176 | }; 177 | rb.print_char( 178 | index, 179 | height, 180 | RustBoxStyle::empty(), 181 | Color::Black, 182 | Color::Byte(19), 183 | ch, 184 | ); 185 | } 186 | 187 | if buffer.dirty { 188 | let data = ['[', '*', ']']; 189 | for (idx, ch) in data.iter().enumerate() { 190 | rb.print_char( 191 | status_text_len + idx + 1, 192 | height, 193 | RustBoxStyle::empty(), 194 | Color::Black, 195 | Color::Red, 196 | *ch, 197 | ); 198 | } 199 | } 200 | if let Some((message, _time)) = self.message { 201 | for (offset, ch) in message.chars().enumerate() { 202 | rb.print_char( 203 | offset, 204 | height + 1, 205 | RustBoxStyle::empty(), 206 | Color::White, 207 | Color::Black, 208 | ch, 209 | ); 210 | } 211 | } 212 | } 213 | 214 | // draw cursor on view 215 | fn draw_cursor(&mut self, rb: &mut RustBox) { 216 | let buffer = self.buffer.lock().unwrap(); 217 | if let Some(top_line) = buffer.get_mark_display_coords(self.top_line) { 218 | if let Some((x, y)) = buffer.get_mark_display_coords(self.cursor) { 219 | rb.set_cursor( 220 | (x - self.left_col) as isize, 221 | y as isize - top_line.1 as isize, 222 | ); 223 | } 224 | } 225 | } 226 | 227 | /// Display the given message 228 | pub fn show_message(&mut self, message: &'static str) { 229 | // let msg = "❆❆❆ ".to_string(); 230 | // let msg = msg + message; 231 | self.message = Some((message, SystemTime::now())); 232 | } 233 | 234 | /// Clear the currently displayed message if it has been there for 5 or more seconds 235 | /// 236 | /// Does nothing if there is no message, or of the message has been there for 237 | /// less that five seconds. 238 | pub fn maybe_clear_message(&mut self) { 239 | if let Some((_message, time)) = self.message { 240 | if let Ok(elapsed) = time.elapsed() { 241 | if elapsed.as_secs() >= 5 { 242 | self.message = None; 243 | } 244 | } 245 | } 246 | } 247 | 248 | pub fn move_mark(&mut self, mark: Mark, object: TextObject) { 249 | self.buffer.lock().unwrap().set_mark_to_object(mark, object); 250 | self.maybe_move_screen(); 251 | } 252 | 253 | /// Update the top_line mark if necessary to keep the cursor on the screen. 254 | fn maybe_move_screen(&mut self) { 255 | let mut buffer = self.buffer.lock().unwrap(); 256 | if let (Some(cursor), Some((_, top_line))) = ( 257 | buffer.get_mark_display_coords(self.cursor), 258 | buffer.get_mark_display_coords(self.top_line), 259 | ) { 260 | let width = (self.get_width() - self.threshold) as isize; 261 | let height = (self.get_height() - self.threshold) as isize; 262 | 263 | //left-right shifting 264 | self.left_col = match cursor.0 as isize - self.left_col as isize { 265 | x_offset if x_offset < self.threshold as isize => cmp::max( 266 | 0, 267 | self.left_col as isize - (self.threshold as isize - x_offset), 268 | ) as usize, 269 | x_offset if x_offset >= width => self.left_col + (x_offset - width + 1) as usize, 270 | _ => self.left_col, 271 | }; 272 | 273 | //up-down shifting 274 | match cursor.1 as isize - top_line as isize { 275 | y_offset if y_offset < self.threshold as isize && top_line > 0 => { 276 | let amount = (self.threshold as isize - y_offset) as usize; 277 | let obj = TextObject { 278 | kind: Kind::Line(Anchor::Same), 279 | offset: Offset::Backward(amount, self.top_line), 280 | }; 281 | buffer.set_mark_to_object(self.top_line, obj); 282 | } 283 | y_offset if y_offset >= height => { 284 | let amount = (y_offset - height + 1) as usize; 285 | let obj = TextObject { 286 | kind: Kind::Line(Anchor::Same), 287 | offset: Offset::Forward(amount, self.top_line), 288 | }; 289 | buffer.set_mark_to_object(self.top_line, obj); 290 | } 291 | _ => {} 292 | } 293 | } 294 | } 295 | 296 | pub fn delete_from_mark_to_object(&mut self, mark: Mark, object: TextObject) { 297 | let mut buffer = self.buffer.lock().unwrap(); 298 | if let Some(mark_pos) = buffer.get_object_index(object) { 299 | if let Some(midx) = buffer.get_mark_idx(mark) { 300 | buffer.remove_from_mark_to_object(mark, object); 301 | buffer.set_mark(mark, cmp::min(mark_pos.absolute, midx)); 302 | } 303 | } 304 | } 305 | 306 | pub fn delete_selection(&mut self) { 307 | // TODO: Implement proper selection? Lines are used for now. 308 | self.move_mark(Mark::Cursor(0), View::selection_start()); 309 | self.delete_from_mark_to_object(Mark::Cursor(0), View::selection_end()); 310 | } 311 | 312 | pub fn get_selection(&mut self) -> Option> { 313 | let mut buffer = self.buffer.lock().unwrap(); 314 | 315 | let start = buffer 316 | .get_object_index(View::selection_start()) 317 | .unwrap() 318 | .absolute; 319 | let end = buffer 320 | .get_object_index(View::selection_end()) 321 | .unwrap() 322 | .absolute; 323 | 324 | buffer.get_range(start, end) 325 | } 326 | 327 | pub fn copy_selection(&mut self) { 328 | let content = self.get_selection().unwrap(); 329 | 330 | let clipboard = ClipboardProvider::new(); 331 | 332 | if clipboard.is_ok() { 333 | let mut ctx: ClipboardContext = clipboard.unwrap(); 334 | ctx.set_contents(content.into_iter().collect()).ok(); 335 | } else { 336 | self.clipboard = content.into_iter().collect(); 337 | } 338 | } 339 | 340 | pub fn duplicate_selection(&mut self) { 341 | let content = self.get_selection(); 342 | self.insert_string(content.unwrap().into_iter().collect()); 343 | } 344 | 345 | pub fn cut_selection(&mut self) { 346 | self.copy_selection(); 347 | self.delete_selection(); 348 | } 349 | 350 | pub fn paste(&mut self) { 351 | let clipboard = ClipboardProvider::new(); 352 | 353 | let content = if clipboard.is_ok() { 354 | let mut ctx: ClipboardContext = clipboard.unwrap(); 355 | ctx.get_contents().unwrap_or(String::from("")) 356 | } else { 357 | self.clipboard.clone() 358 | }; 359 | 360 | self.insert_string(content) 361 | } 362 | 363 | pub fn move_selection(&mut self, down: bool) { 364 | // FIXME: This should probably be one undo/redo transaction. 365 | // Currently, this creates a remove followed by an insert. 366 | if down { 367 | self.move_mark( 368 | Mark::Cursor(0), 369 | TextObject { 370 | kind: Kind::Selection(Anchor::End), 371 | offset: Offset::Forward(0, Mark::Cursor(0)), 372 | }, 373 | ); 374 | 375 | self.move_mark( 376 | Mark::Cursor(0), 377 | TextObject { 378 | kind: Kind::Char, 379 | offset: Offset::Forward(1, Mark::Cursor(0)), 380 | }, 381 | ); 382 | 383 | let content = self.get_selection(); 384 | self.delete_selection(); 385 | 386 | self.move_mark( 387 | Mark::Cursor(0), 388 | TextObject { 389 | kind: Kind::Selection(Anchor::Start), 390 | offset: Offset::Backward(1, Mark::Cursor(0)), 391 | }, 392 | ); 393 | 394 | self.insert_string(content.unwrap().into_iter().collect()); 395 | } else { 396 | let content = self.get_selection(); 397 | self.delete_selection(); 398 | 399 | self.move_mark( 400 | Mark::Cursor(0), 401 | TextObject { 402 | kind: Kind::Selection(Anchor::Start), 403 | offset: Offset::Backward(1, Mark::Cursor(0)), 404 | }, 405 | ); 406 | 407 | self.insert_string(content.unwrap().into_iter().collect()); 408 | 409 | self.move_mark( 410 | Mark::Cursor(0), 411 | TextObject { 412 | kind: Kind::Selection(Anchor::Start), 413 | offset: Offset::Backward(1, Mark::Cursor(0)), 414 | }, 415 | ); 416 | }; 417 | } 418 | 419 | /// Insert a chacter into the buffer & update cursor position accordingly. 420 | pub fn insert_char(&mut self, ch: char) { 421 | self.insert_string(ch.to_string()) 422 | } 423 | 424 | /// Insert a string into the buffer & update cursor position accordingly. 425 | pub fn insert_string(&mut self, s: String) { 426 | let len = self.buffer.lock().unwrap().insert_string(self.cursor, s); 427 | // NOTE: the last param to char_width here may not be correct 428 | if len.unwrap() > 0 { 429 | let obj = TextObject { 430 | kind: Kind::Char, 431 | offset: Offset::Forward(len.unwrap(), Mark::Cursor(0)), 432 | }; 433 | self.move_mark(Mark::Cursor(0), obj) 434 | } 435 | } 436 | 437 | pub fn undo(&mut self) { 438 | { 439 | let mut buffer = self.buffer.lock().unwrap(); 440 | let point = if let Some(transaction) = buffer.undo() { 441 | transaction.end_point 442 | } else { 443 | return; 444 | }; 445 | buffer.set_mark(self.cursor, point); 446 | } 447 | self.maybe_move_screen(); 448 | } 449 | 450 | pub fn redo(&mut self) { 451 | { 452 | let mut buffer = self.buffer.lock().unwrap(); 453 | let point = if let Some(transaction) = buffer.redo() { 454 | transaction.end_point 455 | } else { 456 | return; 457 | }; 458 | buffer.set_mark(self.cursor, point); 459 | } 460 | self.maybe_move_screen(); 461 | } 462 | 463 | fn save_buffer(&mut self) { 464 | let buffer = self.buffer.lock().unwrap(); 465 | let path = match buffer.file_path { 466 | Some(ref p) => Cow::Borrowed(p), 467 | None => { 468 | Cow::Owned(PathBuf::from("untitled")) 469 | }, 470 | }; 471 | 472 | let tmp_path = Path::new(".iotatmp"); 473 | let mut file = match File::create(tmp_path){ 474 | Ok(f) => f, 475 | Err(e) => panic!("error: {}", e) 476 | }; 477 | 478 | for line in buffer.lines() { 479 | let result = file.write_all(line.into_bytes().as_slice()); 480 | if result.is_err() { 481 | panic!("Something went wrong while writing the file"); 482 | } 483 | } 484 | 485 | if let Err(e) = rename(tmp_path, &*path) { 486 | panic!("file error: {}", e); 487 | } 488 | } 489 | 490 | 491 | pub fn try_save_buffer(&mut self) { 492 | let mut should_save = false; 493 | { 494 | let buffer = self.buffer.lock().unwrap(); 495 | match buffer.file_path { 496 | Some(_) => { 497 | should_save = true; 498 | } 499 | None => { 500 | self.message = Some(("Without filename new create under development.", 501 | SystemTime::now())); 502 | } 503 | } 504 | } 505 | 506 | if should_save { 507 | self.save_buffer(); 508 | let mut buffer = self.buffer.lock().unwrap(); 509 | buffer.dirty = false; 510 | } 511 | } 512 | 513 | /// Whether or not the current buffer has unsaved changes 514 | pub fn buffer_is_dirty(&mut self) -> bool { 515 | self.buffer.lock().unwrap().dirty 516 | } 517 | } 518 | 519 | pub fn draw_line(rb: &mut RustBox, line: String, idx: usize, left: usize) { 520 | let width = rb.width() - 1; 521 | let mut x = 0; 522 | 523 | for ch in line.chars().skip(left) { 524 | match ch { 525 | '\t' => { 526 | let w = 4 - x % 4; 527 | for _ in 0..w { 528 | rb.print_char( 529 | x, 530 | idx, 531 | RustBoxStyle::empty(), 532 | Color::White, 533 | Color::Black, 534 | ' ', 535 | ); 536 | x += 1; 537 | } 538 | } 539 | '\n' => {} 540 | _ => { 541 | rb.print_char( 542 | x, 543 | idx, 544 | RustBoxStyle::empty(), 545 | Color::White, 546 | Color::Black, 547 | ch, 548 | ); 549 | x += UnicodeWidthChar::width(ch).unwrap_or(1); 550 | } 551 | } 552 | if x >= width { 553 | break; 554 | } 555 | } 556 | 557 | // Replace any cells after end of line with ' ' 558 | while x < width { 559 | rb.print_char( 560 | x, 561 | idx, 562 | RustBoxStyle::empty(), 563 | Color::White, 564 | Color::Black, 565 | ' ', 566 | ); 567 | x += 1; 568 | } 569 | 570 | // If the line is too long to fit on the screen, show an indicator 571 | let indicator = if line.len() > width + left { 572 | '→' 573 | } else { 574 | ' ' 575 | }; 576 | rb.print_char( 577 | width, 578 | idx, 579 | RustBoxStyle::empty(), 580 | Color::White, 581 | Color::Black, 582 | indicator, 583 | ); 584 | } 585 | 586 | #[cfg(test)] 587 | mod tests { 588 | use buffer::Buffer; 589 | use std::sync::{Arc, Mutex}; 590 | use view::View; 591 | 592 | fn setup_view(testcase: &'static str) -> View { 593 | let buffer = Arc::new(Mutex::new(Buffer::new())); 594 | let mut view = View::new(buffer.clone(), 50, 50); 595 | for ch in testcase.chars() { 596 | view.insert_char(ch); 597 | } 598 | 599 | let mut buffer = buffer.lock().unwrap(); 600 | buffer.set_mark(view.cursor, 0); 601 | view 602 | } 603 | 604 | #[test] 605 | fn test_insert_char() { 606 | let mut view = setup_view("test\nsecond"); 607 | view.insert_char('t'); 608 | 609 | { 610 | let buffer = view.buffer.lock().unwrap(); 611 | assert_eq!(buffer.lines().next().unwrap(), "ttest\n"); 612 | } 613 | } 614 | 615 | #[test] 616 | fn test_insert_string() { 617 | let mut view = setup_view("test\nsecond"); 618 | view.insert_string(String::from("test!")); 619 | 620 | { 621 | let buffer = view.buffer.lock().unwrap(); 622 | assert_eq!(buffer.lines().next().unwrap(), "test!test\n"); 623 | } 624 | } 625 | 626 | #[test] 627 | fn test_cut_copy_paste() { 628 | // It's important to keep clipboard tests together 629 | // The clipboard is a shared resource, and the test runner is multithreaded 630 | let mut view = setup_view("first\nsecond\n"); 631 | view.cut_selection(); 632 | view.paste(); 633 | view.copy_selection(); 634 | view.paste(); 635 | 636 | let buffer = view.buffer.lock().unwrap(); 637 | let mut lines = buffer.lines(); 638 | assert_eq!(lines.next().unwrap(), "first\n"); 639 | assert_eq!(lines.next().unwrap(), "second\n"); 640 | assert_eq!(lines.next().unwrap(), "second\n"); 641 | } 642 | 643 | #[test] 644 | fn test_duplicate_selection() { 645 | let mut view = setup_view("first\nsecond\nthird"); 646 | view.duplicate_selection(); 647 | 648 | { 649 | let buffer = view.buffer.lock().unwrap(); 650 | let mut lines = buffer.lines(); 651 | assert_eq!(lines.next().unwrap(), "first\n"); 652 | assert_eq!(lines.next().unwrap(), "first\n"); 653 | assert_eq!(lines.next().unwrap(), "second\n"); 654 | } 655 | } 656 | 657 | #[test] 658 | fn test_delete_selection() { 659 | let mut view = setup_view("first\nsecond\nthird"); 660 | view.delete_selection(); 661 | 662 | { 663 | let buffer = view.buffer.lock().unwrap(); 664 | assert_eq!(buffer.lines().next().unwrap(), "second\n"); 665 | } 666 | } 667 | 668 | #[test] 669 | fn test_move_selection() { 670 | let mut view = setup_view("test\nsecond\nthird"); 671 | view.move_selection(true); 672 | 673 | { 674 | let buffer = view.buffer.lock().unwrap(); 675 | assert_eq!(buffer.lines().next().unwrap(), "second\n"); 676 | } 677 | } 678 | 679 | #[test] 680 | fn test_move_selection_small() { 681 | let mut view = setup_view("test\nsecond\n"); 682 | view.move_selection(true); 683 | 684 | { 685 | let buffer = view.buffer.lock().unwrap(); 686 | assert_eq!(buffer.lines().next().unwrap(), "second\n"); 687 | } 688 | } 689 | } 690 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(test))] 2 | 3 | extern crate m; 4 | extern crate rustbox; 5 | 6 | use m::{Editor, Input}; 7 | use rustbox::{InitOptions, InputMode, OutputMode, RustBox}; 8 | use std::env; 9 | use std::io::stdin; 10 | 11 | fn main() { 12 | let source = if env::args().len() > 1 { 13 | Input::Filename(env::args().nth(1)) 14 | } else { 15 | Input::Stdin(stdin()) 16 | }; 17 | 18 | let rb = match RustBox::init(InitOptions { 19 | buffer_stderr: false, 20 | input_mode: InputMode::Esc, 21 | output_mode: OutputMode::EightBit, 22 | }) { 23 | Result::Ok(v) => v, 24 | Result::Err(e) => panic!("{}", e), 25 | }; 26 | 27 | let mut editor = Editor::new(source, rb); 28 | editor.start(); 29 | } 30 | --------------------------------------------------------------------------------