├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── elm.json ├── src └── Main.elm ├── tests ├── CodeEditor.elm ├── Tests │ ├── Common.elm │ ├── GoToHoveredPosition.elm │ ├── Hover.elm │ ├── InsertChar.elm │ ├── Invariants.elm │ ├── MoveDown.elm │ ├── MoveLeft.elm │ ├── MoveRight.elm │ ├── MoveUp.elm │ ├── NewLine.elm │ ├── NoOp.elm │ ├── RemoveCharAfter.elm │ └── RemoveCharBefore.elm └── elm-package.json └── watch-compile.sh /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | index.html 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: bash 3 | cache: 4 | directories: 5 | - test/elm-stuff/build-artifacts 6 | - sysconfcpus 7 | 8 | os: 9 | - linux 10 | 11 | env: 12 | matrix: 13 | - ELM_VERSION=0.18.0 TARGET_NODE_VERSION=node 14 | # - ELM_VERSION=0.18.0 TARGET_NODE_VERSION=4.0 15 | 16 | before_install: 17 | - echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config 18 | - | # epic build time improvement - see https://github.com/elm-lang/elm-compiler/issues/1473#issuecomment-245704142 19 | if [ ! -d sysconfcpus/bin ]; 20 | then 21 | git clone https://github.com/obmarg/libsysconfcpus.git; 22 | cd libsysconfcpus; 23 | ./configure --prefix=$TRAVIS_BUILD_DIR/sysconfcpus; 24 | make && make install; 25 | cd ..; 26 | fi 27 | install: 28 | - nvm install $TARGET_NODE_VERSION 29 | - nvm use $TARGET_NODE_VERSION 30 | - node --version 31 | - npm --version 32 | - cd tests 33 | - npm install -g elm@$ELM_VERSION elm-test 34 | - mv $(npm config get prefix)/bin/elm-make $(npm config get prefix)/bin/elm-make-old 35 | - printf '%s\n\n' '#!/bin/bash' 'echo "Running elm-make with sysconfcpus -n 2"' '$TRAVIS_BUILD_DIR/sysconfcpus/bin/sysconfcpus -n 2 elm-make-old "$@"' > $(npm config get prefix)/bin/elm-make 36 | - chmod +x $(npm config get prefix)/bin/elm-make 37 | - elm package install --yes 38 | - cd .. 39 | 40 | script: 41 | - elm-test --fuzz 10000 tests/CodeEditor.elm 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Martin Janiczek 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Travis CI build status](https://travis-ci.org/Janiczek/elm-editor.svg?branch=master) 2 | 3 | Code editor written in Elm 4 | 5 | ### TODO 6 | 7 | - [ ] add context menu functionality 8 | - [ ] add copy paste 9 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.2", 10 | "elm/core": "1.0.5", 11 | "elm/html": "1.0.0", 12 | "elm/json": "1.1.3" 13 | }, 14 | "indirect": { 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm/virtual-dom": "1.0.2" 18 | } 19 | }, 20 | "test-dependencies": { 21 | "direct": {}, 22 | "indirect": {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (..) 2 | 3 | import Array exposing (Array) 4 | import Browser 5 | import Browser.Dom 6 | import Html as H exposing (Attribute, Html) 7 | import Html.Attributes as HA 8 | import Html.Events as HE 9 | import Json.Decode as JD exposing (Decoder) 10 | import Task 11 | 12 | 13 | main : Program () Model Msg 14 | main = 15 | Browser.document 16 | { init = init 17 | , update = update 18 | , view = view 19 | , subscriptions = subscriptions 20 | } 21 | 22 | 23 | type alias Model = 24 | { lines : Array String 25 | , cursor : Position 26 | , hover : Hover 27 | , selection : Selection 28 | } 29 | 30 | 31 | type Hover 32 | = NoHover 33 | | HoverLine Int 34 | | HoverChar Position 35 | 36 | 37 | type Selection 38 | = NoSelection 39 | | SelectingFrom Hover 40 | | SelectedChar Position 41 | | Selection Position Position 42 | 43 | 44 | type alias Position = 45 | { line : Int 46 | , column : Int 47 | } 48 | 49 | 50 | type Msg 51 | = NoOp 52 | | MoveUp 53 | | MoveDown 54 | | MoveLeft 55 | | MoveRight 56 | | NewLine 57 | | InsertChar Char 58 | | RemoveCharBefore 59 | | RemoveCharAfter 60 | | Hover Hover 61 | | GoToHoveredPosition 62 | | StartSelecting 63 | | StopSelecting 64 | 65 | 66 | initModel : Model 67 | initModel = 68 | { lines = Array.fromList [ "" ] 69 | , cursor = Position 0 0 70 | , hover = NoHover 71 | , selection = NoSelection 72 | } 73 | 74 | 75 | init : () -> ( Model, Cmd Msg ) 76 | init () = 77 | ( initModel 78 | , Browser.Dom.focus "editor" 79 | |> Task.attempt (always NoOp) 80 | ) 81 | 82 | 83 | subscriptions : Model -> Sub Msg 84 | subscriptions model = 85 | Sub.none 86 | 87 | 88 | keyDecoder : Decoder Msg 89 | keyDecoder = 90 | JD.field "key" JD.string 91 | |> JD.andThen keyToMsg 92 | 93 | 94 | keyToMsg : String -> Decoder Msg 95 | keyToMsg string = 96 | case String.uncons string of 97 | Just ( char, "" ) -> 98 | JD.succeed (InsertChar char) 99 | 100 | _ -> 101 | case string of 102 | "ArrowUp" -> 103 | JD.succeed MoveUp 104 | 105 | "ArrowDown" -> 106 | JD.succeed MoveDown 107 | 108 | "ArrowLeft" -> 109 | JD.succeed MoveLeft 110 | 111 | "ArrowRight" -> 112 | JD.succeed MoveRight 113 | 114 | "Backspace" -> 115 | JD.succeed RemoveCharBefore 116 | 117 | "Delete" -> 118 | JD.succeed RemoveCharAfter 119 | 120 | "Enter" -> 121 | JD.succeed NewLine 122 | 123 | _ -> 124 | JD.fail "This key does nothing" 125 | 126 | 127 | update : Msg -> Model -> ( Model, Cmd Msg ) 128 | update msg model = 129 | case msg of 130 | NoOp -> 131 | ( model, Cmd.none ) 132 | 133 | MoveUp -> 134 | ( { model | cursor = moveUp model.cursor model.lines } 135 | , Cmd.none 136 | ) 137 | 138 | MoveDown -> 139 | ( { model | cursor = moveDown model.cursor model.lines } 140 | , Cmd.none 141 | ) 142 | 143 | MoveLeft -> 144 | ( { model | cursor = moveLeft model.cursor model.lines } 145 | , Cmd.none 146 | ) 147 | 148 | MoveRight -> 149 | ( { model | cursor = moveRight model.cursor model.lines } 150 | , Cmd.none 151 | ) 152 | 153 | NewLine -> 154 | ( newLine model 155 | |> sanitizeHover 156 | , Cmd.none 157 | ) 158 | 159 | InsertChar char -> 160 | ( insertChar char model 161 | , Cmd.none 162 | ) 163 | 164 | RemoveCharBefore -> 165 | ( removeCharBefore model 166 | |> sanitizeHover 167 | , Cmd.none 168 | ) 169 | 170 | RemoveCharAfter -> 171 | ( removeCharAfter model 172 | |> sanitizeHover 173 | , Cmd.none 174 | ) 175 | 176 | Hover hover -> 177 | ( { model | hover = hover } 178 | |> sanitizeHover 179 | , Cmd.none 180 | ) 181 | 182 | GoToHoveredPosition -> 183 | ( { model 184 | | cursor = 185 | case model.hover of 186 | NoHover -> 187 | model.cursor 188 | 189 | HoverLine line -> 190 | { line = line 191 | , column = lastColumn model.lines line 192 | } 193 | 194 | HoverChar position -> 195 | position 196 | } 197 | , Cmd.none 198 | ) 199 | 200 | StartSelecting -> 201 | ( { model | selection = SelectingFrom model.hover } 202 | , Cmd.none 203 | ) 204 | 205 | StopSelecting -> 206 | -- Selection for all other 207 | let 208 | endHover = 209 | model.hover 210 | 211 | newSelection = 212 | case model.selection of 213 | NoSelection -> 214 | NoSelection 215 | 216 | SelectingFrom startHover -> 217 | if startHover == endHover then 218 | case startHover of 219 | NoHover -> 220 | NoSelection 221 | 222 | HoverLine _ -> 223 | NoSelection 224 | 225 | HoverChar position -> 226 | SelectedChar position 227 | 228 | else 229 | hoversToPositions model.lines startHover endHover 230 | |> Maybe.map (\( from, to ) -> Selection from to) 231 | |> Maybe.withDefault NoSelection 232 | 233 | SelectedChar _ -> 234 | NoSelection 235 | 236 | Selection _ _ -> 237 | NoSelection 238 | in 239 | ( { model | selection = newSelection } 240 | , Cmd.none 241 | ) 242 | 243 | 244 | hoversToPositions : Array String -> Hover -> Hover -> Maybe ( Position, Position ) 245 | hoversToPositions lines from to = 246 | let 247 | selectionLinePosition : Int -> Position -> ( Position, Position ) 248 | selectionLinePosition line position = 249 | if line >= position.line then 250 | ( position 251 | , { line = line, column = lastColumn lines line } 252 | ) 253 | 254 | else 255 | ( { line = line + 1, column = 0 } 256 | , position 257 | ) 258 | in 259 | case ( from, to ) of 260 | ( NoHover, _ ) -> 261 | Nothing 262 | 263 | ( _, NoHover ) -> 264 | Nothing 265 | 266 | ( HoverLine line1, HoverLine line2 ) -> 267 | let 268 | smaller = 269 | min line1 line2 270 | 271 | bigger = 272 | max line1 line2 273 | in 274 | Just 275 | ( { line = smaller + 1, column = 0 } 276 | , { line = bigger, column = lastColumn lines bigger } 277 | ) 278 | 279 | ( HoverLine line, HoverChar position ) -> 280 | Just (selectionLinePosition line position) 281 | 282 | ( HoverChar position, HoverLine line ) -> 283 | Just (selectionLinePosition line position) 284 | 285 | ( HoverChar position1, HoverChar position2 ) -> 286 | let 287 | ( smaller, bigger ) = 288 | if comparePositions position1 position2 == LT then 289 | ( position1, position2 ) 290 | 291 | else 292 | ( position2, position1 ) 293 | in 294 | Just ( smaller, bigger ) 295 | 296 | 297 | comparePositions : Position -> Position -> Order 298 | comparePositions from to = 299 | if from.line < to.line || (from.line == to.line && from.column < to.column) then 300 | LT 301 | 302 | else if from == to then 303 | EQ 304 | 305 | else 306 | GT 307 | 308 | 309 | sanitizeHover : Model -> Model 310 | sanitizeHover model = 311 | { model 312 | | hover = 313 | case model.hover of 314 | NoHover -> 315 | model.hover 316 | 317 | HoverLine line -> 318 | HoverLine (clamp 0 (lastLine model.lines) line) 319 | 320 | HoverChar { line, column } -> 321 | let 322 | sanitizedLine = 323 | clamp 0 (lastLine model.lines) line 324 | 325 | sanitizedColumn = 326 | clamp 0 (lastColumn model.lines sanitizedLine) column 327 | in 328 | HoverChar 329 | { line = sanitizedLine 330 | , column = sanitizedColumn 331 | } 332 | } 333 | 334 | 335 | newLine : Model -> Model 336 | newLine ({ cursor, lines } as model) = 337 | let 338 | { line, column } = 339 | cursor 340 | 341 | linesList : List String 342 | linesList = 343 | Array.toList lines 344 | 345 | line_ : Int 346 | line_ = 347 | line + 1 348 | 349 | contentUntilCursor : List String 350 | contentUntilCursor = 351 | linesList 352 | |> List.take line_ 353 | |> List.indexedMap 354 | (\i content -> 355 | if i == line then 356 | String.left column content 357 | 358 | else 359 | content 360 | ) 361 | 362 | restOfLineAfterCursor : String 363 | restOfLineAfterCursor = 364 | String.dropLeft column (lineContent lines line) 365 | 366 | restOfLines : List String 367 | restOfLines = 368 | List.drop line_ linesList 369 | 370 | newLines : Array String 371 | newLines = 372 | (contentUntilCursor 373 | ++ [ restOfLineAfterCursor ] 374 | ++ restOfLines 375 | ) 376 | |> Array.fromList 377 | 378 | newCursor : Position 379 | newCursor = 380 | { line = line_ 381 | , column = 0 382 | } 383 | in 384 | { model 385 | | lines = newLines 386 | , cursor = newCursor 387 | } 388 | 389 | 390 | insertChar : Char -> Model -> Model 391 | insertChar char ({ cursor, lines } as model) = 392 | let 393 | { line, column } = 394 | cursor 395 | 396 | lineWithCharAdded : String -> String 397 | lineWithCharAdded content = 398 | String.left column content 399 | ++ String.fromChar char 400 | ++ String.dropLeft column content 401 | 402 | newLines : Array String 403 | newLines = 404 | lines 405 | |> Array.indexedMap 406 | (\i content -> 407 | if i == line then 408 | lineWithCharAdded content 409 | 410 | else 411 | content 412 | ) 413 | 414 | newCursor : Position 415 | newCursor = 416 | { line = line 417 | , column = column + 1 418 | } 419 | in 420 | { model 421 | | lines = newLines 422 | , cursor = newCursor 423 | } 424 | 425 | 426 | removeCharBefore : Model -> Model 427 | removeCharBefore ({ cursor, lines } as model) = 428 | if isStartOfDocument cursor then 429 | model 430 | 431 | else 432 | let 433 | { line, column } = 434 | cursor 435 | 436 | lineIsEmpty : Bool 437 | lineIsEmpty = 438 | lineContent lines line 439 | |> String.isEmpty 440 | 441 | removeCharFromLine : ( Int, String ) -> List String 442 | removeCharFromLine ( lineNum, content ) = 443 | if lineNum == line - 1 then 444 | if isFirstColumn column then 445 | [ content ++ lineContent lines line ] 446 | 447 | else 448 | [ content ] 449 | 450 | else if lineNum == line then 451 | if isFirstColumn column then 452 | [] 453 | 454 | else 455 | [ String.left (column - 1) content 456 | ++ String.dropLeft column content 457 | ] 458 | 459 | else 460 | [ content ] 461 | 462 | newLines : Array String 463 | newLines = 464 | lines 465 | |> Array.toIndexedList 466 | |> List.concatMap removeCharFromLine 467 | |> Array.fromList 468 | in 469 | { model 470 | | lines = newLines 471 | , cursor = moveLeft cursor lines 472 | } 473 | 474 | 475 | removeCharAfter : Model -> Model 476 | removeCharAfter ({ cursor, lines } as model) = 477 | if isEndOfDocument lines cursor then 478 | model 479 | 480 | else 481 | let 482 | { line, column } = 483 | cursor 484 | 485 | isOnLastColumn : Bool 486 | isOnLastColumn = 487 | isLastColumn lines line column 488 | 489 | removeCharFromLine : ( Int, String ) -> List String 490 | removeCharFromLine ( lineNum, content ) = 491 | if lineNum == line then 492 | if isOnLastColumn then 493 | [ content ++ lineContent lines (line + 1) ] 494 | 495 | else 496 | [ String.left column content 497 | ++ String.dropLeft (column + 1) content 498 | ] 499 | 500 | else if lineNum == line + 1 then 501 | if isOnLastColumn then 502 | [] 503 | 504 | else 505 | [ content ] 506 | 507 | else 508 | [ content ] 509 | 510 | newLines : Array String 511 | newLines = 512 | lines 513 | |> Array.toIndexedList 514 | |> List.concatMap removeCharFromLine 515 | |> Array.fromList 516 | in 517 | { model 518 | | lines = newLines 519 | , cursor = cursor 520 | } 521 | 522 | 523 | moveUp : Position -> Array String -> Position 524 | moveUp { line, column } lines = 525 | if isFirstLine line then 526 | startOfDocument 527 | 528 | else 529 | let 530 | line_ : Int 531 | line_ = 532 | previousLine line 533 | in 534 | { line = line_ 535 | , column = clampColumn lines line_ column 536 | } 537 | 538 | 539 | moveDown : Position -> Array String -> Position 540 | moveDown { line, column } lines = 541 | if isLastLine lines line then 542 | endOfDocument lines 543 | 544 | else 545 | let 546 | line_ : Int 547 | line_ = 548 | nextLine lines line 549 | in 550 | { line = line_ 551 | , column = clampColumn lines line_ column 552 | } 553 | 554 | 555 | moveLeft : Position -> Array String -> Position 556 | moveLeft ({ line, column } as position) lines = 557 | if isStartOfDocument position then 558 | position 559 | 560 | else if isFirstColumn column then 561 | let 562 | line_ : Int 563 | line_ = 564 | previousLine line 565 | in 566 | { line = line_ 567 | , column = lastColumn lines line_ 568 | } 569 | 570 | else 571 | { line = line 572 | , column = column - 1 573 | } 574 | 575 | 576 | moveRight : Position -> Array String -> Position 577 | moveRight ({ line, column } as position) lines = 578 | if isEndOfDocument lines position then 579 | position 580 | 581 | else if isLastColumn lines line column then 582 | { line = nextLine lines line 583 | , column = 0 584 | } 585 | 586 | else 587 | { line = line 588 | , column = column + 1 589 | } 590 | 591 | 592 | startOfDocument : Position 593 | startOfDocument = 594 | { line = 0 595 | , column = 0 596 | } 597 | 598 | 599 | endOfDocument : Array String -> Position 600 | endOfDocument lines = 601 | { line = lastLine lines 602 | , column = lastColumn lines (lastLine lines) 603 | } 604 | 605 | 606 | isStartOfDocument : Position -> Bool 607 | isStartOfDocument { line, column } = 608 | isFirstLine line 609 | && isFirstColumn column 610 | 611 | 612 | isEndOfDocument : Array String -> Position -> Bool 613 | isEndOfDocument lines { line, column } = 614 | isLastLine lines line 615 | && isLastColumn lines line column 616 | 617 | 618 | isFirstLine : Int -> Bool 619 | isFirstLine line = 620 | line == 0 621 | 622 | 623 | isLastLine : Array String -> Int -> Bool 624 | isLastLine lines line = 625 | line == lastLine lines 626 | 627 | 628 | isFirstColumn : Int -> Bool 629 | isFirstColumn column = 630 | column == 0 631 | 632 | 633 | isLastColumn : Array String -> Int -> Int -> Bool 634 | isLastColumn lines line column = 635 | column == lastColumn lines line 636 | 637 | 638 | lastLine : Array String -> Int 639 | lastLine lines = 640 | Array.length lines - 1 641 | 642 | 643 | previousLine : Int -> Int 644 | previousLine line = 645 | (line - 1) 646 | |> max 0 647 | 648 | 649 | nextLine : Array String -> Int -> Int 650 | nextLine lines line = 651 | (line + 1) 652 | |> min (maxLine lines) 653 | 654 | 655 | maxLine : Array String -> Int 656 | maxLine lines = 657 | Array.length lines - 1 658 | 659 | 660 | lastColumn : Array String -> Int -> Int 661 | lastColumn lines line = 662 | lineLength lines line 663 | 664 | 665 | clampColumn : Array String -> Int -> Int -> Int 666 | clampColumn lines line column = 667 | column 668 | |> clamp 0 (lineLength lines line) 669 | 670 | 671 | lineContent : Array String -> Int -> String 672 | lineContent lines lineNum = 673 | lines 674 | |> Array.get lineNum 675 | |> Maybe.withDefault "" 676 | 677 | 678 | lineLength : Array String -> Int -> Int 679 | lineLength lines lineNum = 680 | lineContent lines lineNum 681 | |> String.length 682 | 683 | 684 | view : Model -> Browser.Document Msg 685 | view model = 686 | { title = "elm-editor" 687 | , body = 688 | [ viewEditor model 689 | , viewDebug model 690 | ] 691 | } 692 | 693 | 694 | viewDebug : Model -> Html Msg 695 | viewDebug { lines, cursor, hover, selection } = 696 | H.div 697 | [ HA.style "max-width" "100%" ] 698 | [ H.text "lines:" 699 | , H.pre 700 | [ HA.style "white-space" "pre-wrap" ] 701 | [ H.text (Debug.toString lines) ] 702 | , H.text "cursor:" 703 | , H.pre [] [ H.text (Debug.toString cursor) ] 704 | , H.text "hover:" 705 | , H.pre [] [ H.text (Debug.toString hover) ] 706 | , H.text "selection:" 707 | , H.pre [] [ H.text (Debug.toString selection) ] 708 | , H.text "selected text:" 709 | , H.pre [] [ H.text (selectedText selection hover lines) ] 710 | ] 711 | 712 | 713 | selectedText : Selection -> Hover -> Array String -> String 714 | selectedText selection currentHover lines = 715 | let 716 | positionsToString : Position -> Position -> String 717 | positionsToString from to = 718 | let 719 | numberOfLines = 720 | to.line - from.line + 1 721 | in 722 | lines 723 | |> Array.toList 724 | |> List.drop from.line 725 | |> List.take numberOfLines 726 | |> List.indexedMap 727 | (\i line -> 728 | if numberOfLines == 1 then 729 | line 730 | |> String.dropLeft from.column 731 | |> String.left (to.column - from.column + 1) 732 | 733 | else if i == 0 then 734 | String.dropLeft from.column line 735 | 736 | else if i == numberOfLines - 1 then 737 | String.left (to.column + 1) line 738 | 739 | else 740 | line 741 | ) 742 | |> String.join "\n" 743 | in 744 | case selection of 745 | NoSelection -> 746 | "" 747 | 748 | SelectingFrom startHover -> 749 | hoversToPositions lines startHover currentHover 750 | |> Maybe.map (\( from, to ) -> positionsToString from to) 751 | |> Maybe.withDefault "" 752 | 753 | SelectedChar { line, column } -> 754 | lineContent lines line 755 | |> String.dropLeft column 756 | |> String.left 1 757 | 758 | Selection from to -> 759 | positionsToString from to 760 | 761 | 762 | viewEditor : Model -> Html Msg 763 | viewEditor model = 764 | H.div 765 | [ HA.style "display" "flex" 766 | , HA.style "flex-direction" "row" 767 | , HA.style "font-family" "monospace" 768 | , HA.style "font-size" (String.fromFloat fontSize ++ "px") 769 | , HA.style "line-height" (String.fromFloat lineHeight ++ "px") 770 | , HA.style "white-space" "pre" 771 | , HE.on "keydown" keyDecoder 772 | , HA.tabindex 0 773 | , HA.id "editor" 774 | ] 775 | [ viewLineNumbers model 776 | , viewContent model 777 | ] 778 | 779 | 780 | viewLineNumbers : Model -> Html Msg 781 | viewLineNumbers model = 782 | H.div 783 | [ HA.style "width" "2em" 784 | , HA.style "text-align" "center" 785 | , HA.style "color" "#888" 786 | , HA.style "display" "flex" 787 | , HA.style "flex-direction" "column" 788 | ] 789 | (List.range 1 (Array.length model.lines) 790 | |> List.map viewLineNumber 791 | ) 792 | 793 | 794 | viewLineNumber : Int -> Html Msg 795 | viewLineNumber n = 796 | H.span [] [ H.text (String.fromInt n) ] 797 | 798 | 799 | viewContent : Model -> Html Msg 800 | viewContent model = 801 | H.div 802 | [ HA.style "position" "relative" 803 | , HA.style "flex" "1" 804 | , HA.style "background-color" "#f0f0f0" 805 | , HA.style "user-select" "none" 806 | , HE.onMouseDown StartSelecting 807 | , HE.onMouseUp StopSelecting 808 | , HE.onClick GoToHoveredPosition 809 | , HE.onMouseOut (Hover NoHover) 810 | ] 811 | [ viewLines model.cursor model.hover model.selection model.lines ] 812 | 813 | 814 | viewLines : Position -> Hover -> Selection -> Array String -> Html Msg 815 | viewLines position hover selection lines = 816 | H.div [] 817 | (lines 818 | |> Array.indexedMap (viewLine position hover selection lines) 819 | |> Array.toList 820 | ) 821 | 822 | 823 | viewLine : Position -> Hover -> Selection -> Array String -> Int -> String -> Html Msg 824 | viewLine position hover selection lines line content = 825 | H.div 826 | [ HA.style "position" "absolute" 827 | , HA.style "left" "0" 828 | , HA.style "right" "0" 829 | , HA.style "height" (String.fromFloat lineHeight ++ "px") 830 | , HA.style "top" (String.fromFloat (toFloat line * lineHeight) ++ "px") 831 | , HE.onMouseOver (Hover (HoverLine line)) 832 | ] 833 | (if position.line == line && isLastColumn lines line position.column then 834 | viewChars position hover selection lines line content 835 | ++ [ viewCursor position nbsp ] 836 | 837 | else 838 | viewChars position hover selection lines line content 839 | ) 840 | 841 | 842 | viewChars : Position -> Hover -> Selection -> Array String -> Int -> String -> List (Html Msg) 843 | viewChars position hover selection lines line content = 844 | content 845 | |> String.toList 846 | |> List.indexedMap (viewChar position hover selection lines line) 847 | 848 | 849 | viewChar : Position -> Hover -> Selection -> Array String -> Int -> Int -> Char -> Html Msg 850 | viewChar position hover selection lines line column char = 851 | if position.line == line && position.column == column then 852 | viewCursor 853 | position 854 | (String.fromChar char) 855 | 856 | else if selection /= NoSelection && isSelected lines selection hover line column then 857 | viewSelectedChar 858 | { line = line, column = column } 859 | (String.fromChar char) 860 | 861 | else 862 | H.span 863 | [ onHover { line = line, column = column } ] 864 | [ H.text (String.fromChar char) ] 865 | 866 | 867 | isSelected : Array String -> Selection -> Hover -> Int -> Int -> Bool 868 | isSelected lines selection currentHover line column = 869 | let 870 | isSelectedPositions : Position -> Position -> Bool 871 | isSelectedPositions from to = 872 | (from.line <= line) 873 | && (to.line >= line) 874 | && (if from.line == line then 875 | from.column <= column 876 | 877 | else 878 | True 879 | ) 880 | && (if to.line == line then 881 | to.column >= column 882 | 883 | else 884 | True 885 | ) 886 | in 887 | case selection of 888 | NoSelection -> 889 | False 890 | 891 | SelectingFrom startHover -> 892 | hoversToPositions lines startHover currentHover 893 | |> Maybe.map (\( from, to ) -> isSelectedPositions from to) 894 | |> Maybe.withDefault False 895 | 896 | SelectedChar position -> 897 | position == { line = line, column = column } 898 | 899 | Selection from to -> 900 | isSelectedPositions from to 901 | 902 | 903 | nbsp : String 904 | nbsp = 905 | "\u{00A0}" 906 | 907 | 908 | viewCursor : Position -> String -> Html Msg 909 | viewCursor position char = 910 | H.span 911 | [ HA.style "background-color" "orange" 912 | , onHover position 913 | ] 914 | [ H.text char ] 915 | 916 | 917 | viewSelectedChar : Position -> String -> Html Msg 918 | viewSelectedChar position char = 919 | H.span 920 | [ HA.style "background-color" "#ccc" 921 | , onHover position 922 | ] 923 | [ H.text char ] 924 | 925 | 926 | onHover : Position -> Attribute Msg 927 | onHover position = 928 | HE.custom "mouseover" 929 | (JD.succeed 930 | { message = Hover (HoverChar position) 931 | , stopPropagation = True 932 | , preventDefault = True 933 | } 934 | ) 935 | 936 | 937 | fontSize : Float 938 | fontSize = 939 | 20 940 | 941 | 942 | lineHeight : Float 943 | lineHeight = 944 | fontSize * 1.2 945 | -------------------------------------------------------------------------------- /tests/CodeEditor.elm: -------------------------------------------------------------------------------- 1 | module CodeEditor exposing (..) 2 | 3 | import Test exposing (..) 4 | import Tests.GoToHoveredPosition as GoToHoveredPosition 5 | import Tests.Hover as Hover 6 | import Tests.InsertChar as InsertChar 7 | import Tests.Invariants as Invariants 8 | import Tests.MoveDown as MoveDown 9 | import Tests.MoveLeft as MoveLeft 10 | import Tests.MoveRight as MoveRight 11 | import Tests.MoveUp as MoveUp 12 | import Tests.NewLine as NewLine 13 | import Tests.NoOp as NoOp 14 | import Tests.RemoveCharAfter as RemoveCharAfter 15 | import Tests.RemoveCharBefore as RemoveCharBefore 16 | 17 | 18 | suite : Test 19 | suite = 20 | concat 21 | [ describe "Invariants" 22 | [ describe "cursor" 23 | [ describe "line" 24 | [ Invariants.cursorLineIsAlwaysPositive 25 | , Invariants.cursorLineNeverGetsToNonexistingLine 26 | ] 27 | , describe "column" 28 | [ Invariants.cursorColumnIsAlwaysPositive 29 | , Invariants.cursorColumnNeverGetsMoreThanOneCharAfterLineContents 30 | ] 31 | ] 32 | , describe "lines" 33 | [ Invariants.linesArrayNeverEmpty 34 | ] 35 | , describe "hover" 36 | [ Invariants.hoverAlwaysWithinBounds 37 | ] 38 | , todo "selection" 39 | ] 40 | , describe "Msgs" 41 | [ describe "NoOp" 42 | [ NoOp.doesNothing 43 | ] 44 | , describe "MoveUp" 45 | [ MoveUp.jumpsToStartOfLineIfOnFirstLine 46 | , MoveUp.movesUpALineIfNotOnFirstLine 47 | , MoveUp.staysOnSameColumnIfNotOnFirstLineAndEnoughCharsAboveCursor 48 | , MoveUp.movesToLastColumnIfNotOnFirstLineAndNoCharAboveCursor 49 | ] 50 | , describe "MoveDown" 51 | [ MoveDown.jumpsToEndOfLineIfOnLastLine 52 | , MoveDown.movesDownALineIfNotOnLastLine 53 | , MoveDown.staysOnSameColumnIfNotOnLastLineAndEnoughCharsBelowCursor 54 | , MoveDown.movesToLastColumnIfNotOnLastLineAndNoCharBelowCursor 55 | ] 56 | , describe "MoveLeft" 57 | [ MoveLeft.doesNothingOnStartOfDocument 58 | , MoveLeft.movesLeftIfNotOnFirstColumn 59 | , MoveLeft.movesToEndOfPreviousLineIfOnTheFirstColumnAndNotOnFirstLine 60 | ] 61 | , describe "MoveRight" 62 | [ MoveRight.doesNothingOnEndOfDocument 63 | , MoveRight.movesRightIfNotOnLastColumn 64 | , MoveRight.movesToStartOfNextLineIfOnTheLastColumnAndNotOnLastLine 65 | ] 66 | , describe "NewLine" 67 | [ NewLine.movesDownALine 68 | , NewLine.movesToFirstColumn 69 | , NewLine.addsALine 70 | , NewLine.splitsALineIntoTwo 71 | ] 72 | , describe "InsertChar" 73 | [ InsertChar.movesRight 74 | , InsertChar.doesntMoveUpOrDown 75 | , InsertChar.makesLineLonger 76 | , InsertChar.insertsChar 77 | ] 78 | , describe "RemoveCharBefore" 79 | [ RemoveCharBefore.doesNothingOnStartOfDocument 80 | , RemoveCharBefore.decreasesLineCountOnFirstColumnOfNotFirstLine 81 | , RemoveCharBefore.combinesLinesTogetherOnFirstColumnOfNotFirstLine 82 | , RemoveCharBefore.movesUpALineOnFirstColumnOfNotFirstLine 83 | , RemoveCharBefore.movesToPreviousEndOfPreviousLineOnFirstColumnOfNotFirstLine 84 | , RemoveCharBefore.decreasesCurrentLineLengthOnNotFirstColumn 85 | , RemoveCharBefore.movesLeftOnNotFirstColumn 86 | , RemoveCharBefore.doesntMoveUpOrDownOnNotFirstColumn 87 | , RemoveCharBefore.removesTheCharOnNotFirstColumn 88 | ] 89 | , describe "RemoveCharAfter" 90 | [ RemoveCharAfter.doesNothingOnEndOfDocument 91 | , RemoveCharAfter.doesntMove 92 | , RemoveCharAfter.decreasesLineCountOnLastColumnOfNotLastLine 93 | , RemoveCharAfter.combinesLinesTogetherOnLastColumnOfNotLastLine 94 | , RemoveCharAfter.decreasesCurrentLineLengthOnNotLastColumn 95 | , RemoveCharAfter.removesTheCharOnNotLastColumn 96 | ] 97 | , describe "Hover" 98 | [ Hover.setsHoverWithinBounds 99 | ] 100 | , describe "GoToHoveredPosition" 101 | [ GoToHoveredPosition.doesNothingIfHoverIsNoHover 102 | , GoToHoveredPosition.movesToLastColumnOfHoveredLineIfHoverIsHoverLine 103 | , GoToHoveredPosition.movesToHoveredPositionIfHoverIsHoverChar 104 | ] 105 | , todo "StartSelecting" 106 | , todo "StopSelecting" 107 | ] 108 | ] 109 | -------------------------------------------------------------------------------- /tests/Tests/Common.elm: -------------------------------------------------------------------------------- 1 | module Tests.Common exposing (..) 2 | 3 | import ArchitectureTest.Types exposing (..) 4 | import Fuzz exposing (Fuzzer, int, list, string) 5 | import Main exposing (..) 6 | 7 | 8 | app : TestedApp Model Msg 9 | app = 10 | { model = ConstantModel initModel 11 | , update = NormalUpdate update 12 | , msgFuzzer = msgFuzzer 13 | } 14 | 15 | 16 | msgFuzzer : Fuzzer Msg 17 | msgFuzzer = 18 | Fuzz.oneOf 19 | [ noOp 20 | , moveUp 21 | , moveDown 22 | , moveLeft 23 | , moveRight 24 | , newLine 25 | , insertChar 26 | , removeCharBefore 27 | , removeCharAfter 28 | , hover 29 | , goToHoveredPosition 30 | ] 31 | 32 | 33 | noOp : Fuzzer Msg 34 | noOp = 35 | Fuzz.constant NoOp 36 | 37 | 38 | moveUp : Fuzzer Msg 39 | moveUp = 40 | Fuzz.constant MoveUp 41 | 42 | 43 | moveDown : Fuzzer Msg 44 | moveDown = 45 | Fuzz.constant MoveDown 46 | 47 | 48 | moveLeft : Fuzzer Msg 49 | moveLeft = 50 | Fuzz.constant MoveLeft 51 | 52 | 53 | moveRight : Fuzzer Msg 54 | moveRight = 55 | Fuzz.constant MoveRight 56 | 57 | 58 | newLine : Fuzzer Msg 59 | newLine = 60 | Fuzz.constant NewLine 61 | 62 | 63 | insertChar : Fuzzer Msg 64 | insertChar = 65 | Fuzz.char |> Fuzz.map InsertChar 66 | 67 | 68 | removeCharBefore : Fuzzer Msg 69 | removeCharBefore = 70 | Fuzz.constant RemoveCharBefore 71 | 72 | 73 | removeCharAfter : Fuzzer Msg 74 | removeCharAfter = 75 | Fuzz.constant RemoveCharAfter 76 | 77 | 78 | hover : Fuzzer Msg 79 | hover = 80 | Fuzz.oneOf 81 | [ Fuzz.constant NoHover 82 | , Fuzz.map HoverLine Fuzz.int 83 | , Fuzz.map2 (\line column -> HoverChar { line = line, column = column }) Fuzz.int Fuzz.int 84 | ] 85 | |> Fuzz.map Hover 86 | 87 | 88 | goToHoveredPosition : Fuzzer Msg 89 | goToHoveredPosition = 90 | Fuzz.constant GoToHoveredPosition 91 | -------------------------------------------------------------------------------- /tests/Tests/GoToHoveredPosition.elm: -------------------------------------------------------------------------------- 1 | module Tests.GoToHoveredPosition exposing (..) 2 | 3 | import ArchitectureTest exposing (..) 4 | import Expect exposing (Expectation) 5 | import Main exposing (..) 6 | import Test exposing (..) 7 | import Tests.Common exposing (..) 8 | 9 | 10 | doesNothingIfHoverIsNoHover : Test 11 | doesNothingIfHoverIsNoHover = 12 | msgTestWithPrecondition "GoToHoveredPosition does nothing if hover is NoHover" 13 | app 14 | goToHoveredPosition 15 | (\model -> model.hover == NoHover) 16 | <| 17 | \_ _ modelBeforeMsg _ finalModel -> 18 | modelBeforeMsg 19 | |> Expect.equal finalModel 20 | 21 | 22 | movesToLastColumnOfHoveredLineIfHoverIsHoverLine : Test 23 | movesToLastColumnOfHoveredLineIfHoverIsHoverLine = 24 | msgTestWithPrecondition "GoToHoveredPosition moves to last column of hovered line if hover is HoverLine" 25 | app 26 | goToHoveredPosition 27 | (\model -> 28 | case model.hover of 29 | NoHover -> 30 | False 31 | 32 | HoverLine _ -> 33 | True 34 | 35 | HoverChar _ -> 36 | False 37 | ) 38 | <| 39 | \_ _ modelBeforeMsg _ finalModel -> 40 | case modelBeforeMsg.hover of 41 | NoHover -> 42 | Expect.fail "hover should have been HoverLine" 43 | 44 | HoverLine line -> 45 | finalModel.cursor 46 | |> Expect.equal 47 | { line = line 48 | , column = lastColumn modelBeforeMsg.lines line 49 | } 50 | 51 | HoverChar _ -> 52 | Expect.fail "hover should have been HoverLine" 53 | 54 | 55 | movesToHoveredPositionIfHoverIsHoverChar : Test 56 | movesToHoveredPositionIfHoverIsHoverChar = 57 | msgTestWithPrecondition "GoToHoveredPosition moves to hovered position if hover is HoverChar" 58 | app 59 | goToHoveredPosition 60 | (\model -> 61 | case model.hover of 62 | NoHover -> 63 | False 64 | 65 | HoverLine _ -> 66 | False 67 | 68 | HoverChar _ -> 69 | True 70 | ) 71 | <| 72 | \_ _ modelBeforeMsg _ finalModel -> 73 | case modelBeforeMsg.hover of 74 | NoHover -> 75 | Expect.fail "hover should have been HoverChar" 76 | 77 | HoverLine _ -> 78 | Expect.fail "hover should have been HoverChar" 79 | 80 | HoverChar { line, column } -> 81 | finalModel.cursor 82 | |> Expect.equal 83 | { line = line 84 | , column = column 85 | } 86 | -------------------------------------------------------------------------------- /tests/Tests/Hover.elm: -------------------------------------------------------------------------------- 1 | module Tests.Hover exposing (..) 2 | 3 | import ArchitectureTest exposing (..) 4 | import Expect exposing (Expectation) 5 | import Main exposing (..) 6 | import Test exposing (..) 7 | import Tests.Common exposing (..) 8 | 9 | 10 | setsHoverWithinBounds : Test 11 | setsHoverWithinBounds = 12 | msgTest "Hover sets hover within bounds" app hover <| 13 | \_ _ modelBeforeMsg msg finalModel -> 14 | case msg of 15 | Hover hover -> 16 | case hover of 17 | NoHover -> 18 | finalModel.hover 19 | |> Expect.equal NoHover 20 | 21 | HoverLine _ -> 22 | case finalModel.hover of 23 | NoHover -> 24 | Expect.fail "hover should have been HoverLine" 25 | 26 | HoverLine line -> 27 | line 28 | |> Expect.all 29 | [ Expect.atLeast 0 30 | , Expect.atMost (lastLine modelBeforeMsg.lines) 31 | ] 32 | 33 | HoverChar _ -> 34 | Expect.fail "hover should have been HoverLine" 35 | 36 | HoverChar _ -> 37 | case finalModel.hover of 38 | NoHover -> 39 | Expect.fail "hover should have been HoverChar" 40 | 41 | HoverLine _ -> 42 | Expect.fail "hover should have been HoverChar" 43 | 44 | HoverChar position -> 45 | position 46 | |> Expect.all 47 | [ .line >> Expect.atLeast 0 48 | , .line >> Expect.atMost (lastLine modelBeforeMsg.lines) 49 | , .column >> Expect.atLeast 0 50 | , \{ line, column } -> 51 | column 52 | |> Expect.atMost (lastColumn modelBeforeMsg.lines line) 53 | ] 54 | 55 | _ -> 56 | Expect.pass 57 | -------------------------------------------------------------------------------- /tests/Tests/InsertChar.elm: -------------------------------------------------------------------------------- 1 | module Tests.InsertChar exposing (..) 2 | 3 | import ArchitectureTest exposing (..) 4 | import Array.Hamt as Array 5 | import Expect exposing (Expectation) 6 | import Main exposing (Msg(..), lineContent, lineLength) 7 | import Test exposing (..) 8 | import Tests.Common exposing (..) 9 | 10 | 11 | movesRight : Test 12 | movesRight = 13 | msgTest "InsertChar moves right" 14 | app 15 | insertChar 16 | <| 17 | \_ _ modelBeforeMsg _ finalModel -> 18 | finalModel.cursor.column 19 | |> Expect.equal (modelBeforeMsg.cursor.column + 1) 20 | 21 | 22 | doesntMoveUpOrDown : Test 23 | doesntMoveUpOrDown = 24 | msgTest "InsertChar doesn't move up or down" 25 | app 26 | insertChar 27 | <| 28 | \_ _ modelBeforeMsg _ finalModel -> 29 | finalModel.cursor.line 30 | |> Expect.equal modelBeforeMsg.cursor.line 31 | 32 | 33 | makesLineLonger : Test 34 | makesLineLonger = 35 | msgTest "InsertChar makes line longer" 36 | app 37 | insertChar 38 | <| 39 | \_ _ modelBeforeMsg _ finalModel -> 40 | lineLength finalModel.lines finalModel.cursor.line 41 | |> Expect.equal (lineLength modelBeforeMsg.lines modelBeforeMsg.cursor.line + 1) 42 | 43 | 44 | insertsChar : Test 45 | insertsChar = 46 | msgTest "InsertChar inserts char" 47 | app 48 | insertChar 49 | <| 50 | \_ _ modelBeforeMsg msg finalModel -> 51 | case msg of 52 | InsertChar char -> 53 | let 54 | charString = 55 | String.fromChar char 56 | 57 | newLine = 58 | lineContent finalModel.lines finalModel.cursor.line 59 | 60 | oldLine = 61 | lineContent modelBeforeMsg.lines modelBeforeMsg.cursor.line 62 | 63 | beforeCursor = 64 | String.left modelBeforeMsg.cursor.column oldLine 65 | 66 | afterCursor = 67 | String.dropLeft modelBeforeMsg.cursor.column oldLine 68 | in 69 | newLine 70 | |> Expect.equal (beforeCursor ++ charString ++ afterCursor) 71 | 72 | _ -> 73 | Expect.pass 74 | -------------------------------------------------------------------------------- /tests/Tests/Invariants.elm: -------------------------------------------------------------------------------- 1 | module Tests.Invariants exposing (..) 2 | 3 | import ArchitectureTest exposing (..) 4 | import Array.Hamt as Array 5 | import Expect exposing (Expectation) 6 | import Main exposing (..) 7 | import Test exposing (..) 8 | import Tests.Common exposing (..) 9 | 10 | 11 | cursorLineIsAlwaysPositive : Test 12 | cursorLineIsAlwaysPositive = 13 | invariantTest "cursor.line is always positive" app <| 14 | \_ _ finalModel -> 15 | finalModel.cursor.line 16 | |> Expect.atLeast 0 17 | 18 | 19 | cursorLineNeverGetsToNonexistingLine : Test 20 | cursorLineNeverGetsToNonexistingLine = 21 | invariantTest "cursor.line never gets to nonexisting line" app <| 22 | \_ _ finalModel -> 23 | finalModel.cursor.line 24 | |> Expect.atMost (lastLine finalModel.lines) 25 | 26 | 27 | cursorColumnIsAlwaysPositive : Test 28 | cursorColumnIsAlwaysPositive = 29 | invariantTest "cursor.column is always positive" app <| 30 | \_ _ finalModel -> 31 | finalModel.cursor.column 32 | |> Expect.atLeast 0 33 | 34 | 35 | cursorColumnNeverGetsMoreThanOneCharAfterLineContents : Test 36 | cursorColumnNeverGetsMoreThanOneCharAfterLineContents = 37 | invariantTest "cursor.column never gets more than one char after line contents" app <| 38 | \_ _ finalModel -> 39 | finalModel.cursor.column 40 | |> Expect.atMost 41 | (lastColumn 42 | finalModel.lines 43 | finalModel.cursor.line 44 | ) 45 | 46 | 47 | linesArrayNeverEmpty : Test 48 | linesArrayNeverEmpty = 49 | invariantTest "lines array never empty" app <| 50 | \_ _ finalModel -> 51 | finalModel.lines 52 | |> Array.length 53 | |> Expect.atLeast 1 54 | 55 | 56 | hoverAlwaysWithinBounds : Test 57 | hoverAlwaysWithinBounds = 58 | invariantTest "hover always within bounds" app <| 59 | \_ _ finalModel -> 60 | case finalModel.hover of 61 | NoHover -> 62 | Expect.pass 63 | 64 | HoverLine line -> 65 | Expect.all 66 | [ Expect.atLeast 0 67 | , Expect.atMost (lastLine finalModel.lines) 68 | ] 69 | line 70 | 71 | HoverChar position -> 72 | Expect.all 73 | [ .line >> Expect.atLeast 0 74 | , .line >> Expect.atMost (lastLine finalModel.lines) 75 | , .column >> Expect.atLeast 0 76 | , \{ line, column } -> column |> Expect.atMost (lastColumn finalModel.lines line) 77 | ] 78 | position 79 | -------------------------------------------------------------------------------- /tests/Tests/MoveDown.elm: -------------------------------------------------------------------------------- 1 | module Tests.MoveDown exposing (..) 2 | 3 | import ArchitectureTest exposing (..) 4 | import Array.Hamt as Array 5 | import Expect exposing (Expectation) 6 | import Main exposing (endOfDocument, lastColumn, lastLine, lineLength) 7 | import Test exposing (..) 8 | import Tests.Common exposing (..) 9 | 10 | 11 | jumpsToEndOfLineIfOnLastLine : Test 12 | jumpsToEndOfLineIfOnLastLine = 13 | msgTestWithPrecondition "MoveDown jumps to end of line if on last line" 14 | app 15 | moveDown 16 | (\model -> model.cursor.line == lastLine model.lines) 17 | <| 18 | \_ _ _ _ finalModel -> 19 | finalModel.cursor 20 | |> Expect.equal (endOfDocument finalModel.lines) 21 | 22 | 23 | movesDownALineIfNotOnLastLine : Test 24 | movesDownALineIfNotOnLastLine = 25 | msgTestWithPrecondition "MoveDown moves down a line if not on last line" 26 | app 27 | moveDown 28 | (\model -> model.cursor.line /= lastLine model.lines) 29 | <| 30 | \_ _ modelBeforeMsg _ finalModel -> 31 | finalModel.cursor.line 32 | |> Expect.equal (modelBeforeMsg.cursor.line + 1) 33 | 34 | 35 | staysOnSameColumnIfNotOnLastLineAndEnoughCharsBelowCursor : Test 36 | staysOnSameColumnIfNotOnLastLineAndEnoughCharsBelowCursor = 37 | msgTestWithPrecondition "MoveDown stays on same column if not on last line and enough chars below cursor" 38 | app 39 | moveDown 40 | (\model -> 41 | (model.cursor.line /= lastLine model.lines) 42 | && (lineLength model.lines (model.cursor.line + 1) >= model.cursor.column) 43 | ) 44 | <| 45 | \_ _ modelBeforeMsg _ finalModel -> 46 | finalModel.cursor.column 47 | |> Expect.equal modelBeforeMsg.cursor.column 48 | 49 | 50 | movesToLastColumnIfNotOnLastLineAndNoCharBelowCursor : Test 51 | movesToLastColumnIfNotOnLastLineAndNoCharBelowCursor = 52 | msgTestWithPrecondition "MoveDown moves to last column if not on last line and no char below cursor" 53 | app 54 | moveDown 55 | (\model -> 56 | (model.cursor.line /= lastLine model.lines) 57 | && (lineLength model.lines (model.cursor.line + 1) < model.cursor.column) 58 | ) 59 | <| 60 | \_ _ _ _ finalModel -> 61 | finalModel.cursor.column 62 | |> Expect.equal 63 | (lastColumn 64 | finalModel.lines 65 | finalModel.cursor.line 66 | ) 67 | -------------------------------------------------------------------------------- /tests/Tests/MoveLeft.elm: -------------------------------------------------------------------------------- 1 | module Tests.MoveLeft exposing (..) 2 | 3 | import ArchitectureTest exposing (..) 4 | import Array.Hamt as Array 5 | import Expect exposing (Expectation) 6 | import Main exposing (isStartOfDocument, lineLength) 7 | import Test exposing (..) 8 | import Tests.Common exposing (..) 9 | 10 | 11 | doesNothingOnStartOfDocument : Test 12 | doesNothingOnStartOfDocument = 13 | msgTestWithPrecondition "MoveLeft does nothing on start of document" 14 | app 15 | moveLeft 16 | (\model -> isStartOfDocument model.cursor) 17 | <| 18 | \_ _ modelBeforeMsg _ finalModel -> 19 | finalModel 20 | |> Expect.equal modelBeforeMsg 21 | 22 | 23 | movesLeftIfNotOnFirstColumn : Test 24 | movesLeftIfNotOnFirstColumn = 25 | msgTestWithPrecondition "MoveLeft moves left if not on first column" 26 | app 27 | moveLeft 28 | (\model -> model.cursor.column /= 0) 29 | <| 30 | \_ _ modelBeforeMsg _ finalModel -> 31 | finalModel.cursor.column 32 | |> Expect.equal (modelBeforeMsg.cursor.column - 1) 33 | 34 | 35 | movesToEndOfPreviousLineIfOnTheFirstColumnAndNotOnFirstLine : Test 36 | movesToEndOfPreviousLineIfOnTheFirstColumnAndNotOnFirstLine = 37 | msgTestWithPrecondition "MoveLeft moves to end of previous line if on the first column and not on first line" 38 | app 39 | moveLeft 40 | (\model -> model.cursor.column == 0 && model.cursor.line /= 0) 41 | <| 42 | \_ _ modelBeforeMsg _ finalModel -> 43 | Expect.all 44 | [ .line >> Expect.equal (modelBeforeMsg.cursor.line - 1) 45 | , .column 46 | >> Expect.equal 47 | (lineLength 48 | modelBeforeMsg.lines 49 | (modelBeforeMsg.cursor.line - 1) 50 | ) 51 | ] 52 | finalModel.cursor 53 | -------------------------------------------------------------------------------- /tests/Tests/MoveRight.elm: -------------------------------------------------------------------------------- 1 | module Tests.MoveRight exposing (..) 2 | 3 | import ArchitectureTest exposing (..) 4 | import Array.Hamt as Array 5 | import Expect exposing (Expectation) 6 | import Main exposing (isEndOfDocument, lastColumn, lastLine) 7 | import Test exposing (..) 8 | import Tests.Common exposing (..) 9 | 10 | 11 | doesNothingOnEndOfDocument : Test 12 | doesNothingOnEndOfDocument = 13 | msgTestWithPrecondition "MoveRight does nothing on end of document" 14 | app 15 | moveRight 16 | (\model -> isEndOfDocument model.lines model.cursor) 17 | <| 18 | \_ _ modelBeforeMsg _ finalModel -> 19 | finalModel 20 | |> Expect.equal modelBeforeMsg 21 | 22 | 23 | movesRightIfNotOnLastColumn : Test 24 | movesRightIfNotOnLastColumn = 25 | msgTestWithPrecondition "MoveRight moves right if not on last column" 26 | app 27 | moveRight 28 | (\model -> model.cursor.column /= lastColumn model.lines model.cursor.line) 29 | <| 30 | \_ _ modelBeforeMsg _ finalModel -> 31 | finalModel.cursor.column 32 | |> Expect.equal (modelBeforeMsg.cursor.column + 1) 33 | 34 | 35 | movesToStartOfNextLineIfOnTheLastColumnAndNotOnLastLine : Test 36 | movesToStartOfNextLineIfOnTheLastColumnAndNotOnLastLine = 37 | msgTestWithPrecondition "MoveRight moves to start of next line if on the last column and not on last line" 38 | app 39 | moveRight 40 | (\model -> 41 | (model.cursor.column == lastColumn model.lines model.cursor.line) 42 | && (model.cursor.line /= lastLine model.lines) 43 | ) 44 | <| 45 | \_ _ modelBeforeMsg _ finalModel -> 46 | Expect.all 47 | [ .line >> Expect.equal (modelBeforeMsg.cursor.line + 1) 48 | , .column >> Expect.equal 0 49 | ] 50 | finalModel.cursor 51 | -------------------------------------------------------------------------------- /tests/Tests/MoveUp.elm: -------------------------------------------------------------------------------- 1 | module Tests.MoveUp exposing (..) 2 | 3 | import ArchitectureTest exposing (..) 4 | import Array.Hamt as Array 5 | import Expect exposing (Expectation) 6 | import Main exposing (lineLength, startOfDocument) 7 | import Test exposing (..) 8 | import Tests.Common exposing (..) 9 | 10 | 11 | jumpsToStartOfLineIfOnFirstLine : Test 12 | jumpsToStartOfLineIfOnFirstLine = 13 | msgTestWithPrecondition "MoveUp jumps to start of line if on first line" 14 | app 15 | moveUp 16 | (\model -> model.cursor.line == 0) 17 | <| 18 | \_ _ _ _ finalModel -> 19 | finalModel.cursor 20 | |> Expect.equal startOfDocument 21 | 22 | 23 | movesUpALineIfNotOnFirstLine : Test 24 | movesUpALineIfNotOnFirstLine = 25 | msgTestWithPrecondition "MoveUp moves up a line if not on first line" 26 | app 27 | moveUp 28 | (\model -> model.cursor.line /= 0) 29 | <| 30 | \_ _ modelBeforeMsg _ finalModel -> 31 | finalModel.cursor.line 32 | |> Expect.equal (modelBeforeMsg.cursor.line - 1) 33 | 34 | 35 | staysOnSameColumnIfNotOnFirstLineAndEnoughCharsAboveCursor : Test 36 | staysOnSameColumnIfNotOnFirstLineAndEnoughCharsAboveCursor = 37 | msgTestWithPrecondition "MoveUp stays on same column if not on first line and enough chars above cursor" 38 | app 39 | moveUp 40 | (\model -> 41 | (model.cursor.line /= 0) 42 | && (lineLength model.lines (model.cursor.line - 1) >= model.cursor.column) 43 | ) 44 | <| 45 | \_ _ modelBeforeMsg _ finalModel -> 46 | finalModel.cursor.column 47 | |> Expect.equal modelBeforeMsg.cursor.column 48 | 49 | 50 | movesToLastColumnIfNotOnFirstLineAndNoCharAboveCursor : Test 51 | movesToLastColumnIfNotOnFirstLineAndNoCharAboveCursor = 52 | msgTestWithPrecondition "MoveUp moves to last column if not on first line and no char above cursor" 53 | app 54 | moveUp 55 | (\model -> 56 | (model.cursor.line /= 0) 57 | && (lineLength model.lines (model.cursor.line - 1) < model.cursor.column) 58 | ) 59 | <| 60 | \_ _ _ _ finalModel -> 61 | finalModel.cursor.column 62 | |> Expect.equal 63 | (lineLength 64 | finalModel.lines 65 | finalModel.cursor.line 66 | ) 67 | -------------------------------------------------------------------------------- /tests/Tests/NewLine.elm: -------------------------------------------------------------------------------- 1 | module Tests.NewLine exposing (..) 2 | 3 | import ArchitectureTest exposing (..) 4 | import Array.Hamt as Array 5 | import Expect exposing (Expectation) 6 | import Main exposing (lineContent) 7 | import Test exposing (..) 8 | import Tests.Common exposing (..) 9 | 10 | 11 | movesDownALine : Test 12 | movesDownALine = 13 | msgTest "NewLine moves down a line" 14 | app 15 | newLine 16 | <| 17 | \_ _ modelBeforeMsg _ finalModel -> 18 | finalModel.cursor.line 19 | |> Expect.equal (modelBeforeMsg.cursor.line + 1) 20 | 21 | 22 | movesToFirstColumn : Test 23 | movesToFirstColumn = 24 | msgTest "NewLine moves to first column" 25 | app 26 | newLine 27 | <| 28 | \_ _ _ _ finalModel -> 29 | finalModel.cursor.column 30 | |> Expect.equal 0 31 | 32 | 33 | addsALine : Test 34 | addsALine = 35 | msgTest "NewLine adds a line" 36 | app 37 | newLine 38 | <| 39 | \_ _ modelBeforeMsg _ finalModel -> 40 | Array.length finalModel.lines 41 | |> Expect.equal (Array.length modelBeforeMsg.lines + 1) 42 | 43 | 44 | splitsALineIntoTwo : Test 45 | splitsALineIntoTwo = 46 | msgTest "NewLine splits a line into two" 47 | app 48 | newLine 49 | <| 50 | \_ _ modelBeforeMsg _ finalModel -> 51 | let 52 | oldLine = 53 | lineContent modelBeforeMsg.lines modelBeforeMsg.cursor.line 54 | in 55 | Expect.all 56 | [ \lines -> 57 | lineContent lines (finalModel.cursor.line - 1) 58 | |> Expect.equal (String.left modelBeforeMsg.cursor.column oldLine) 59 | , \lines -> 60 | lineContent lines finalModel.cursor.line 61 | |> Expect.equal (String.dropLeft modelBeforeMsg.cursor.column oldLine) 62 | ] 63 | finalModel.lines 64 | -------------------------------------------------------------------------------- /tests/Tests/NoOp.elm: -------------------------------------------------------------------------------- 1 | module Tests.NoOp exposing (..) 2 | 3 | import ArchitectureTest exposing (..) 4 | import Expect exposing (Expectation) 5 | import Test exposing (..) 6 | import Tests.Common exposing (..) 7 | 8 | 9 | doesNothing : Test 10 | doesNothing = 11 | msgTest "NoOp does nothing" app noOp <| 12 | \_ _ modelBeforeMsg _ finalModel -> 13 | modelBeforeMsg 14 | |> Expect.equal finalModel 15 | -------------------------------------------------------------------------------- /tests/Tests/RemoveCharAfter.elm: -------------------------------------------------------------------------------- 1 | module Tests.RemoveCharAfter exposing (..) 2 | 3 | import ArchitectureTest exposing (..) 4 | import Array.Hamt as Array 5 | import Expect exposing (Expectation) 6 | import Main exposing (isEndOfDocument, isLastColumn, isLastLine, lineContent, lineLength) 7 | import Test exposing (..) 8 | import Tests.Common exposing (..) 9 | 10 | 11 | doesNothingOnEndOfDocument : Test 12 | doesNothingOnEndOfDocument = 13 | msgTestWithPrecondition "RemoveCharAfter does nothing on start of document" 14 | app 15 | removeCharAfter 16 | (\model -> isEndOfDocument model.lines model.cursor) 17 | <| 18 | \_ _ modelBeforeMsg _ finalModel -> 19 | finalModel 20 | |> Expect.equal modelBeforeMsg 21 | 22 | 23 | doesntMove : Test 24 | doesntMove = 25 | msgTest "RemoveCharAfter doesn't move" 26 | app 27 | removeCharAfter 28 | <| 29 | \_ _ modelBeforeMsg _ finalModel -> 30 | finalModel.cursor 31 | |> Expect.equal modelBeforeMsg.cursor 32 | 33 | 34 | decreasesLineCountOnLastColumnOfNotLastLine : Test 35 | decreasesLineCountOnLastColumnOfNotLastLine = 36 | msgTestWithPrecondition "RemoveCharAfter decreases line count on last column of not last line" 37 | app 38 | removeCharAfter 39 | (\model -> 40 | isLastColumn model.lines model.cursor.line model.cursor.column 41 | && not (isLastLine model.lines model.cursor.line) 42 | ) 43 | <| 44 | \_ _ modelBeforeMsg _ finalModel -> 45 | Array.length finalModel.lines 46 | |> Expect.equal (Array.length modelBeforeMsg.lines - 1) 47 | 48 | 49 | combinesLinesTogetherOnLastColumnOfNotLastLine : Test 50 | combinesLinesTogetherOnLastColumnOfNotLastLine = 51 | msgTestWithPrecondition "RemoveCharAfter combines lines together on last column of not last line" 52 | app 53 | removeCharAfter 54 | (\model -> 55 | isLastColumn model.lines model.cursor.line model.cursor.column 56 | && not (isLastLine model.lines model.cursor.line) 57 | ) 58 | <| 59 | \_ _ modelBeforeMsg _ finalModel -> 60 | let 61 | oldLine1 = 62 | lineContent modelBeforeMsg.lines modelBeforeMsg.cursor.line 63 | 64 | oldLine2 = 65 | lineContent modelBeforeMsg.lines (modelBeforeMsg.cursor.line + 1) 66 | 67 | expectedNewLine = 68 | oldLine1 ++ oldLine2 69 | 70 | newLine = 71 | lineContent finalModel.lines finalModel.cursor.line 72 | in 73 | newLine 74 | |> Expect.equal expectedNewLine 75 | 76 | 77 | decreasesCurrentLineLengthOnNotLastColumn : Test 78 | decreasesCurrentLineLengthOnNotLastColumn = 79 | msgTestWithPrecondition "RemoveCharAfter decreases current line length on not last column" 80 | app 81 | removeCharAfter 82 | (\model -> not (isLastColumn model.lines model.cursor.line model.cursor.column)) 83 | <| 84 | \_ _ modelBeforeMsg _ finalModel -> 85 | let 86 | newLineLength = 87 | lineLength finalModel.lines finalModel.cursor.line 88 | 89 | oldLineLength = 90 | lineLength modelBeforeMsg.lines modelBeforeMsg.cursor.line 91 | in 92 | newLineLength 93 | |> Expect.equal (oldLineLength - 1) 94 | 95 | 96 | removesTheCharOnNotLastColumn : Test 97 | removesTheCharOnNotLastColumn = 98 | msgTestWithPrecondition "RemoveCharAfter removes the char on not last column" 99 | app 100 | removeCharAfter 101 | (\model -> not (isLastColumn model.lines model.cursor.line model.cursor.column)) 102 | <| 103 | \_ _ modelBeforeMsg _ finalModel -> 104 | let 105 | oldLine = 106 | lineContent modelBeforeMsg.lines modelBeforeMsg.cursor.line 107 | 108 | oldLineBefore = 109 | oldLine 110 | |> String.left modelBeforeMsg.cursor.column 111 | 112 | oldLineAfter = 113 | oldLine 114 | |> String.dropLeft (modelBeforeMsg.cursor.column + 1) 115 | 116 | expectedNewLine = 117 | oldLineBefore ++ oldLineAfter 118 | 119 | newLine = 120 | lineContent finalModel.lines finalModel.cursor.line 121 | in 122 | newLine 123 | |> Expect.equal expectedNewLine 124 | -------------------------------------------------------------------------------- /tests/Tests/RemoveCharBefore.elm: -------------------------------------------------------------------------------- 1 | module Tests.RemoveCharBefore exposing (..) 2 | 3 | import ArchitectureTest exposing (..) 4 | import Array.Hamt as Array 5 | import Expect exposing (Expectation) 6 | import Main exposing (isStartOfDocument, lineContent, lineLength) 7 | import Test exposing (..) 8 | import Tests.Common exposing (..) 9 | 10 | 11 | doesNothingOnStartOfDocument : Test 12 | doesNothingOnStartOfDocument = 13 | msgTestWithPrecondition "RemoveCharBefore does nothing on start of document" 14 | app 15 | removeCharBefore 16 | (\model -> isStartOfDocument model.cursor) 17 | <| 18 | \_ _ modelBeforeMsg _ finalModel -> 19 | finalModel 20 | |> Expect.equal modelBeforeMsg 21 | 22 | 23 | decreasesLineCountOnFirstColumnOfNotFirstLine : Test 24 | decreasesLineCountOnFirstColumnOfNotFirstLine = 25 | msgTestWithPrecondition "RemoveCharBefore decreases line count on first column of not first line" 26 | app 27 | removeCharBefore 28 | (\model -> 29 | (model.cursor.column == 0) 30 | && (model.cursor.line /= 0) 31 | ) 32 | <| 33 | \_ _ modelBeforeMsg _ finalModel -> 34 | Array.length finalModel.lines 35 | |> Expect.equal (Array.length modelBeforeMsg.lines - 1) 36 | 37 | 38 | combinesLinesTogetherOnFirstColumnOfNotFirstLine : Test 39 | combinesLinesTogetherOnFirstColumnOfNotFirstLine = 40 | msgTestWithPrecondition "RemoveCharBefore combines lines together on first column of not first line" 41 | app 42 | removeCharBefore 43 | (\model -> 44 | (model.cursor.column == 0) 45 | && (model.cursor.line /= 0) 46 | ) 47 | <| 48 | \_ _ modelBeforeMsg _ finalModel -> 49 | let 50 | newLine = 51 | lineContent finalModel.lines finalModel.cursor.line 52 | 53 | oldLine1 = 54 | lineContent modelBeforeMsg.lines (modelBeforeMsg.cursor.line - 1) 55 | 56 | oldLine2 = 57 | lineContent modelBeforeMsg.lines modelBeforeMsg.cursor.line 58 | in 59 | newLine 60 | |> Expect.equal (oldLine1 ++ oldLine2) 61 | 62 | 63 | movesUpALineOnFirstColumnOfNotFirstLine : Test 64 | movesUpALineOnFirstColumnOfNotFirstLine = 65 | msgTestWithPrecondition "RemoveCharBefore moves up a line on first column of not first line" 66 | app 67 | removeCharBefore 68 | (\model -> 69 | (model.cursor.column == 0) 70 | && (model.cursor.line /= 0) 71 | ) 72 | <| 73 | \_ _ modelBeforeMsg _ finalModel -> 74 | finalModel.cursor.line 75 | |> Expect.equal (modelBeforeMsg.cursor.line - 1) 76 | 77 | 78 | movesToPreviousEndOfPreviousLineOnFirstColumnOfNotFirstLine : Test 79 | movesToPreviousEndOfPreviousLineOnFirstColumnOfNotFirstLine = 80 | msgTestWithPrecondition "RemoveCharBefore moves to previous end of previous line on first column of not first line" 81 | app 82 | removeCharBefore 83 | (\model -> 84 | (model.cursor.column == 0) 85 | && (model.cursor.line /= 0) 86 | ) 87 | <| 88 | \_ _ modelBeforeMsg _ finalModel -> 89 | finalModel.cursor.column 90 | |> Expect.equal (lineLength modelBeforeMsg.lines (modelBeforeMsg.cursor.line - 1)) 91 | 92 | 93 | decreasesCurrentLineLengthOnNotFirstColumn : Test 94 | decreasesCurrentLineLengthOnNotFirstColumn = 95 | msgTestWithPrecondition "RemoveCharBefore decreases current line length on not first column" 96 | app 97 | removeCharBefore 98 | (\model -> model.cursor.column /= 0) 99 | <| 100 | \_ _ modelBeforeMsg _ finalModel -> 101 | lineContent finalModel.lines finalModel.cursor.line 102 | |> String.length 103 | |> Expect.equal 104 | ((lineContent modelBeforeMsg.lines modelBeforeMsg.cursor.line 105 | |> String.length 106 | ) 107 | - 1 108 | ) 109 | 110 | 111 | movesLeftOnNotFirstColumn : Test 112 | movesLeftOnNotFirstColumn = 113 | msgTestWithPrecondition "RemoveCharBefore moves left on not first column" 114 | app 115 | removeCharBefore 116 | (\model -> model.cursor.column /= 0) 117 | <| 118 | \_ _ modelBeforeMsg _ finalModel -> 119 | finalModel.cursor.column 120 | |> Expect.equal (modelBeforeMsg.cursor.column - 1) 121 | 122 | 123 | doesntMoveUpOrDownOnNotFirstColumn : Test 124 | doesntMoveUpOrDownOnNotFirstColumn = 125 | msgTestWithPrecondition "RemoveCharBefore doesn't move up or down on not first column" 126 | app 127 | removeCharBefore 128 | (\model -> model.cursor.column /= 0) 129 | <| 130 | \_ _ modelBeforeMsg _ finalModel -> 131 | finalModel.cursor.line 132 | |> Expect.equal modelBeforeMsg.cursor.line 133 | 134 | 135 | removesTheCharOnNotFirstColumn : Test 136 | removesTheCharOnNotFirstColumn = 137 | msgTestWithPrecondition "RemoveCharBefore removes the char on not first column" 138 | app 139 | removeCharBefore 140 | (\model -> model.cursor.column /= 0) 141 | <| 142 | \_ _ modelBeforeMsg _ finalModel -> 143 | let 144 | oldLine = 145 | lineContent modelBeforeMsg.lines modelBeforeMsg.cursor.line 146 | 147 | oldLinePart1 = 148 | oldLine 149 | |> String.left (modelBeforeMsg.cursor.column - 1) 150 | 151 | oldLinePart2 = 152 | oldLine 153 | |> String.dropLeft modelBeforeMsg.cursor.column 154 | 155 | expectedNewLine = 156 | oldLinePart1 ++ oldLinePart2 157 | 158 | newLine = 159 | lineContent finalModel.lines finalModel.cursor.line 160 | in 161 | newLine 162 | |> Expect.equal expectedNewLine 163 | -------------------------------------------------------------------------------- /tests/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Test Suites", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "../src", 8 | "." 9 | ], 10 | "exposed-modules": [], 11 | "dependencies": { 12 | "Janiczek/elm-architecture-test": "1.0.10 <= v < 2.0.0", 13 | "Skinney/elm-array-exploration": "2.0.5 <= v < 3.0.0", 14 | "eeue56/elm-html-test": "5.2.0 <= v < 6.0.0", 15 | "elm-community/elm-test": "4.0.0 <= v < 5.0.0", 16 | "elm-lang/core": "5.1.1 <= v < 6.0.0", 17 | "elm-lang/dom": "1.1.1 <= v < 2.0.0", 18 | "elm-lang/html": "2.0.0 <= v < 3.0.0" 19 | }, 20 | "elm-version": "0.18.0 <= v < 0.19.0" 21 | } 22 | -------------------------------------------------------------------------------- /watch-compile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | COLOR_OFF="\e[0m"; 4 | DIM="\e[2m"; 5 | 6 | function compile { 7 | find src -type f -name '*.elm' | xargs elm make 8 | } 9 | 10 | function run { 11 | clear; 12 | tput reset; 13 | echo -en "\033c\033[3J"; 14 | 15 | echo -en "${DIM}"; 16 | date -R; 17 | echo -en "${COLOR_OFF}"; 18 | 19 | compile; 20 | } 21 | 22 | run; 23 | 24 | chokidar src | while read WHATEVER; do 25 | run; 26 | done; 27 | --------------------------------------------------------------------------------