├── demo
├── make.sh
├── README.md
├── package-lock.json
├── public
│ ├── assets
│ │ ├── math-text.js
│ │ ├── math-text-delayed.js
│ │ ├── outside.js
│ │ ├── custom-element-config.js
│ │ └── style.css
│ └── index.html
├── package.json
├── elm-dev.json
├── elm.json
├── elm-pub.json
└── src
│ ├── Outside.elm
│ ├── AppText.elm
│ └── Demo.elm
├── docs
├── DEVTRICKS.md
├── README_SYDNEY_NEMZER.md
└── Embedding.md
├── testSpares
├── elm-verify-examples.json
├── Tests
│ ├── NoOp.elm
│ ├── Insert.elm
│ ├── CursorLeft.elm
│ ├── Invariants.elm
│ ├── CursorRight.elm
│ └── Common.elm
├── EditorTest.elm
└── UnitTest.elm
├── demo-simple
├── README.md
├── package-lock.json
├── public
│ └── index.html
├── scripts
│ └── publish.sh
├── package.json
├── elm-dev.json
├── elm.json
├── elm-pub.json
└── src
│ ├── DemoSimple.elm
│ └── AppText.elm
├── src
├── Main.elm
├── Editor
│ ├── Style.elm
│ ├── Model.elm
│ ├── History.elm
│ ├── Function.elm
│ ├── Config.elm
│ ├── SearchX.elm
│ ├── Widget.elm
│ ├── Search.elm
│ ├── Styles.elm
│ ├── Strings.elm
│ ├── Keymap.elm
│ ├── Wrap.elm
│ └── View.elm
├── Util
│ └── Array.elm
├── Position.elm
├── BufferHelper.elm
├── Window.elm
├── Buffer2.elm
├── Array
│ └── Util.elm
└── Editor.elm
├── .gitignore
├── package.json
├── elm.json
├── LICENSE
├── README.md
└── tests
├── Buffer2Test.elm
└── BufferComparison.elm
/demo/make.sh:
--------------------------------------------------------------------------------
1 | elm make --debug src/Demo.elm --output public/Demo.js
2 |
--------------------------------------------------------------------------------
/docs/DEVTRICKS.md:
--------------------------------------------------------------------------------
1 | [npx](https://dev.to/sarscode/npx-vs-npm-the-npx-advantage-1h0o)
--------------------------------------------------------------------------------
/testSpares/elm-verify-examples.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": "../src",
3 | "tests": ["Buffer2"]
4 | }
5 |
--------------------------------------------------------------------------------
/demo/README.md:
--------------------------------------------------------------------------------
1 | # To Run
2 |
3 | ````bash
4 |
5 | npm install
6 |
7 | npm start
8 |
9 | ````
10 |
--------------------------------------------------------------------------------
/demo/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1
5 | }
6 |
--------------------------------------------------------------------------------
/demo-simple/README.md:
--------------------------------------------------------------------------------
1 | # To Run
2 |
3 | ````bash
4 |
5 | npm install
6 |
7 | npm start
8 |
9 | ````
10 |
--------------------------------------------------------------------------------
/demo-simple/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo-simple",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1
5 | }
6 |
--------------------------------------------------------------------------------
/src/Main.elm:
--------------------------------------------------------------------------------
1 | module Main exposing (main)
2 |
3 | import Html exposing (Html)
4 |
5 |
6 | main : Html msg
7 | main =
8 | Html.text "Hello, elm-browser!"
9 |
--------------------------------------------------------------------------------
/src/Editor/Style.elm:
--------------------------------------------------------------------------------
1 | module Editor.Style exposing (darkBlue, darkGray, lightBlue, lightGray)
2 |
3 |
4 | lightBlue =
5 | "#8d9ffe"
6 |
7 |
8 | darkBlue =
9 | "#172da3"
10 |
11 |
12 | lightGray =
13 | "#a5a6ab"
14 |
15 |
16 | darkGray =
17 | "#444548"
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | elm-stuff/
3 | .idea/
4 |
5 | demo/public/Demo.js
6 | demo/scripts/publish.sh
7 | demo/public/Demo.min.js
8 | demo/public/index-remote.html
9 | demno/public/DemoSimple.min.js
10 |
11 | demo-simple/public/index-remote.html
12 | demo-simple/*.sh
13 | demo-simple/public/DemoSimple.js
14 | demo-simple/public/DemoSimple.min.js
15 |
16 | tests/VerifyExamples/
17 | markdown/
18 |
19 | docs/NOTES.md
20 |
21 |
--------------------------------------------------------------------------------
/src/Util/Array.elm:
--------------------------------------------------------------------------------
1 | module Util.Array exposing (last)
2 |
3 | {-| Basically Array.Extra but for stuff that isn't included in Array.Extra
4 | -}
5 |
6 | import Array exposing (Array)
7 |
8 |
9 | last : Array a -> Maybe ( a, Int )
10 | last array =
11 | let
12 | length =
13 | Array.length array
14 | in
15 | Array.slice -1 length array
16 | |> Array.get 0
17 | |> Maybe.map (\a -> ( a, length - 1 ))
18 |
--------------------------------------------------------------------------------
/demo-simple/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Simple Text Editor Demo
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/testSpares/Tests/NoOp.elm:
--------------------------------------------------------------------------------
1 | module Tests.NoOp exposing (doesNothing)
2 |
3 | import ArchitectureTest exposing (..)
4 | import Expect exposing (Expectation)
5 | import Fuzz exposing (Fuzzer)
6 | import Test exposing (..)
7 | import Tests.Common exposing (..)
8 |
9 |
10 | type alias MsgTestO model msg =
11 | String
12 | -> TestedApp model msg
13 | -> Fuzzer msg
14 | -> (model -> msg -> model -> Expectation)
15 | -> Test
16 |
17 |
18 | doesNothing : Test
19 | doesNothing =
20 | msgTest "NoOp does nothing" app noOp <|
21 | \modelBeforeMsg _ finalModel ->
22 | modelBeforeMsg
23 | |> Expect.equal finalModel
24 |
--------------------------------------------------------------------------------
/demo/public/assets/math-text.js:
--------------------------------------------------------------------------------
1 | class MathText extends HTMLElement {
2 |
3 | // The paragraph below detects the
4 | // argument to the custom element
5 | // and is necessary for innerHTML
6 | // to receive the argument.
7 | set content(value) {
8 | this.innerHTML = value
9 | }
10 |
11 | connectedCallback() {
12 | this.attachShadow({mode: "open"});
13 | this.shadowRoot.innerHTML =
14 | '' + this.innerHTML + '';
15 | MathJax.typesetShadow(this.shadowRoot);
16 | // setTimeout(() => MathJax.typesetShadow(this.shadowRoot), 1);
17 | }
18 | }
19 |
20 | customElements.define('math-text', MathText)
21 |
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "elm-text-editor",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "directories": {
7 | "test": "tests"
8 | },
9 | "scripts": {
10 | "test": "npx elm-test"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/jxxcarlson/elm-text-editor.git"
15 | },
16 | "keywords": [],
17 | "author": "",
18 | "license": "ISC",
19 | "bugs": {
20 | "url": "https://github.com/jxxcarlson/elm-text-editor/issues"
21 | },
22 | "homepage": "https://github.com/jxxcarlson/elm-text-editor#readme",
23 | "dependencies": {
24 | "elm-json": "^0.2.6"
25 | },
26 | "devDependencies": {
27 | "elm-test": "^0.19.1"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/public/assets/math-text-delayed.js:
--------------------------------------------------------------------------------
1 | class MathTextDelayed extends HTMLElement {
2 |
3 | // The "set content" code below detects the
4 | // argument to the custom element
5 | // and is necessary for innerHTML
6 | // to receive the argument.
7 | set content(value) {
8 | this.innerHTML = value
9 | }
10 |
11 | connectedCallback() {
12 | this.attachShadow({mode: "open"});
13 | this.shadowRoot.innerHTML =
14 | '' + this.innerHTML + '';
15 | // MathJax.typesetShadow(this.shadowRoot);
16 | setTimeout(() => MathJax.typesetShadow(this.shadowRoot), 1);
17 | }
18 | }
19 |
20 | customElements.define('math-text-delayed', MathTextDelayed)
21 |
22 |
--------------------------------------------------------------------------------
/demo-simple/scripts/publish.sh:
--------------------------------------------------------------------------------
1 | color=`tput setaf 48`
2 | reset=`tput setaf 7`
3 |
4 | echo
5 | echo "${color}:Publishing ...${reset}"
6 |
7 | SOURCE=/Users/carlson/dev/elm/mylibraries/elm-text-editor/demo-simple/src
8 | PUBLIC=/Users/carlson/dev/elm/mylibraries/elm-text-editor/demo-simple/public
9 | TARGET=/Users/carlson/dev/github_pages/app/editor-simple
10 |
11 | elm make --optimize ${SOURCE}/DemoSimple.elm --output=${PUBLIC}/DemoSimple.js
12 |
13 | echo
14 | echo "${color}:Uglifying ...${reset}"
15 |
16 | uglifyjs ${PUBLIC}/DemoSimple.js -mc 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9"' -o ${PUBLIC}/DemoSimple.min.js
17 |
18 | echo
19 | echo "${color}:Copying ...${reset}"
20 |
21 | cp ${PUBLIC}/index-remote.html ${TARGET}/index.html
22 | cp ${PUBLIC}/DemoSimple.min.js ${TARGET}/
23 |
24 |
25 | echo
26 | echo "${color}cd /Users/carlson/dev/github_pages${reset}"
27 |
28 |
29 | cd /Users/carlson/dev/github_pages
30 |
--------------------------------------------------------------------------------
/demo/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Pure Elm Text Editor
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "version": "1.0.0",
4 | "description": "Compile with elm make --optimize Main.elm --output=Demo.js",
5 | "main": "Demo.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "elm-live -d public/ src/Demo.elm -- --output public/Demo.js",
9 | "debug": "elm make --debug src/Demo.elm --output public/Demo.js",
10 | "compile": "elm make --optimize src/Demo.elm --output public/Demo.js",
11 | "ff": "open -a /Applications/Firefox.app/ public/index.html",
12 | "chrome": "open -a /Applications/GoogleChrome.app/ public/index.html",
13 | "set-dev": "cp elm-dev.json elm.json",
14 | "set-pub": "cp elm-pub.json elm.json",
15 | "update-pub": "cp elm.json elm-pub.json",
16 | "publish": "sh scripts/publish.sh",
17 | "go" : "open -a /Applications/Firefox.app https://jxxcarlson.github.io/app/editor/index.html"
18 | },
19 | "keywords": [],
20 | "author": "",
21 | "license": "BSD-3"
22 | }
23 |
--------------------------------------------------------------------------------
/demo-simple/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo-simple",
3 | "version": "1.0.0",
4 | "description": "Compile with elm make --optimize Main.elm --output=Demo.js",
5 | "main": "Demo.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "elm-live -d public/ src/DemoSimple.elm -- --output public/DemoSimple.js",
9 | "debug": "elm make --debug src/DemoSimple.elm --output public/DemoSimple.js",
10 | "compile": "elm make --optimize src/DemoSimple.elm --output public/DemoSimple.js",
11 | "ff": "open -a /Applications/Firefox.app/ public/index.html",
12 | "chrome": "open -a /Applications/GoogleChrome.app/ public/index.html",
13 | "set-dev": "cp elm-dev.json elm.json",
14 | "set-pub": "cp elm-pub.json elm.json",
15 | "update-pub": "cp elm.json elm-pub.json",
16 | "go" : "open -a /Applications/GoogleChrome.app https://jxxcarlson.github.io/app/editor-simple/index.html",
17 | "publish": "sh scripts/publish.sh"
18 | },
19 | "keywords": [],
20 | "author": "",
21 | "license": "ISC"
22 | }
23 |
--------------------------------------------------------------------------------
/src/Editor/Model.elm:
--------------------------------------------------------------------------------
1 | module Editor.Model exposing (InternalState, Snapshot)
2 |
3 | import Buffer exposing (Buffer)
4 | import Debounce exposing (Debounce)
5 | import Editor.Config exposing (Config)
6 | import Editor.History exposing (History)
7 | import Position exposing (Position)
8 | import RollingList exposing (RollingList)
9 |
10 |
11 | type alias Snapshot =
12 | { cursor : Position
13 | , selection : Maybe Position
14 | , buffer : Buffer
15 | }
16 |
17 |
18 | type alias InternalState =
19 | { config : Config
20 | , topLine : Int
21 | , cursor : Position
22 | , selection : Maybe Position
23 | , selectedText : Maybe String
24 | , clipboard : String
25 | , currentLine : Maybe String
26 | , dragging : Bool
27 | , history : History Snapshot
28 | , searchTerm : String
29 | , searchHitIndex : Int
30 | , replacementText : String
31 | , canReplace : Bool
32 | , searchResults : RollingList ( Position, Position )
33 | , showHelp : Bool
34 | , showInfoPanel : Bool
35 | , showGoToLinePanel : Bool
36 | , showSearchPanel : Bool
37 | , savedBuffer : Buffer
38 | , debounce : Debounce String
39 | }
40 |
--------------------------------------------------------------------------------
/testSpares/Tests/Insert.elm:
--------------------------------------------------------------------------------
1 | module Tests.Insert exposing (one)
2 |
3 | import ArchitectureTest exposing (..)
4 | import Editor
5 | import Expect exposing (Expectation)
6 | import Fuzz exposing (Fuzzer)
7 | import Test exposing (..)
8 | import Tests.Common exposing (..)
9 |
10 |
11 | one : Test
12 | one =
13 | msgTest "inserting one character properly advances the cursor" app insertOne <|
14 | \initialModel _ finalModel ->
15 | let
16 | initialCursor =
17 | Editor.getCursor initialModel
18 |
19 | finalCursor =
20 | Editor.getCursor finalModel
21 |
22 | ok =
23 | case ( finalCursor.line - initialCursor.line, finalCursor.column - initialCursor.column, finalCursor.column ) of
24 | ( 0, 1, _ ) ->
25 | True
26 |
27 | ( 0, _, _ ) ->
28 | False
29 |
30 | ( 1, _, 0 ) ->
31 | True
32 |
33 | _ ->
34 | False
35 | in
36 | ok
37 | |> Expect.equal True
38 |
--------------------------------------------------------------------------------
/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "package",
3 | "name": "jxxcarlson/elm-text-editor",
4 | "summary": "Pure Elm text editor forked from Sydney Nemzer",
5 | "license": "BSD-3-Clause",
6 | "version": "7.0.8",
7 | "exposed-modules": [
8 | "Editor",
9 | "Editor.Strings",
10 | "Editor.Config",
11 | "Editor.Update"
12 | ],
13 | "elm-version": "0.19.0 <= v < 0.20.0",
14 | "dependencies": {
15 | "Janiczek/architecture-test": "2.1.0 <= v < 3.0.0",
16 | "carwow/elm-slider": "10.0.0 <= v < 11.0.0",
17 | "elm/browser": "1.0.0 <= v < 2.0.0",
18 | "elm/core": "1.0.0 <= v < 2.0.0",
19 | "elm/html": "1.0.0 <= v < 2.0.0",
20 | "elm/json": "1.0.0 <= v < 2.0.0",
21 | "elm-community/list-extra": "8.1.0 <= v < 9.0.0",
22 | "elm-community/maybe-extra": "5.0.0 <= v < 6.0.0",
23 | "elm-community/string-extra": "4.0.0 <= v < 5.0.0",
24 | "elm-explorations/test": "1.2.2 <= v < 2.0.0",
25 | "folkertdev/elm-paragraph": "1.0.0 <= v < 2.0.0",
26 | "jinjor/elm-debounce": "3.0.0 <= v < 4.0.0",
27 | "lovasoa/elm-rolling-list": "1.1.4 <= v < 2.0.0",
28 | "lukewestby/elm-string-interpolate": "1.0.4 <= v < 2.0.0"
29 | },
30 | "test-dependencies": {}
31 | }
32 |
--------------------------------------------------------------------------------
/demo-simple/elm-dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "src",
5 | "../src"
6 | ],
7 | "elm-version": "0.19.1",
8 | "dependencies": {
9 | "direct": {
10 | "elm/browser": "1.0.2",
11 | "elm/core": "1.0.4",
12 | "elm/html": "1.0.0",
13 | "elm/json": "1.1.3",
14 | "elm-community/list-extra": "8.2.3",
15 | "elm-community/maybe-extra": "5.1.0",
16 | "elm-community/string-extra": "4.0.1",
17 | "folkertdev/elm-paragraph": "1.0.0",
18 | "jinjor/elm-debounce": "3.0.0",
19 | "lovasoa/elm-rolling-list": "1.1.4",
20 | "lukewestby/elm-string-interpolate": "1.0.4"
21 | },
22 | "indirect": {
23 | "Janiczek/architecture-test": "2.1.0",
24 | "carwow/elm-slider": "10.0.0",
25 | "debois/elm-dom": "1.3.0",
26 | "elm/random": "1.0.0",
27 | "elm/regex": "1.0.0",
28 | "elm/time": "1.0.0",
29 | "elm/url": "1.0.0",
30 | "elm/virtual-dom": "1.0.2",
31 | "elm-explorations/test": "1.2.2"
32 | }
33 | },
34 | "test-dependencies": {
35 | "direct": {},
36 | "indirect": {}
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/demo-simple/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.4",
11 | "elm/html": "1.0.0",
12 | "elm/json": "1.1.3",
13 | "jxxcarlson/elm-text-editor": "7.0.8"
14 | },
15 | "indirect": {
16 | "Janiczek/architecture-test": "2.1.0",
17 | "carwow/elm-slider": "10.0.0",
18 | "debois/elm-dom": "1.3.0",
19 | "elm/random": "1.0.0",
20 | "elm/regex": "1.0.0",
21 | "elm/time": "1.0.0",
22 | "elm/url": "1.0.0",
23 | "elm/virtual-dom": "1.0.2",
24 | "elm-community/list-extra": "8.2.3",
25 | "elm-community/maybe-extra": "5.1.0",
26 | "elm-community/string-extra": "4.0.1",
27 | "elm-explorations/test": "1.2.2",
28 | "folkertdev/elm-paragraph": "1.0.0",
29 | "jinjor/elm-debounce": "3.0.0",
30 | "lovasoa/elm-rolling-list": "1.1.4",
31 | "lukewestby/elm-string-interpolate": "1.0.4"
32 | }
33 | },
34 | "test-dependencies": {
35 | "direct": {},
36 | "indirect": {}
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/demo-simple/elm-pub.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.4",
11 | "elm/html": "1.0.0",
12 | "elm/json": "1.1.3",
13 | "jxxcarlson/elm-text-editor": "7.0.7"
14 | },
15 | "indirect": {
16 | "Janiczek/architecture-test": "2.1.0",
17 | "carwow/elm-slider": "10.0.0",
18 | "debois/elm-dom": "1.3.0",
19 | "elm/random": "1.0.0",
20 | "elm/regex": "1.0.0",
21 | "elm/time": "1.0.0",
22 | "elm/url": "1.0.0",
23 | "elm/virtual-dom": "1.0.2",
24 | "elm-community/list-extra": "8.2.3",
25 | "elm-community/maybe-extra": "5.1.0",
26 | "elm-community/string-extra": "4.0.1",
27 | "elm-explorations/test": "1.2.2",
28 | "folkertdev/elm-paragraph": "1.0.0",
29 | "jinjor/elm-debounce": "3.0.0",
30 | "lovasoa/elm-rolling-list": "1.1.4",
31 | "lukewestby/elm-string-interpolate": "1.0.4"
32 | }
33 | },
34 | "test-dependencies": {
35 | "direct": {},
36 | "indirect": {}
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 James Carlson
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 |
--------------------------------------------------------------------------------
/src/Editor/History.elm:
--------------------------------------------------------------------------------
1 | module Editor.History exposing (History, empty, push, redo, undo)
2 |
3 | {-| Making changes adds entries to the past. Undoing a few times adds those
4 | entries to the future. Making another change adds a new entry to the past and
5 | deletes the future. Redoing moves entries from the future to the past.
6 | Past entries are older at higher indexes, future entries are newer at higher
7 | indexes
8 | -}
9 |
10 |
11 | type alias InternalHistory a =
12 | { past : List a, future : List a }
13 |
14 |
15 | type History a
16 | = History (InternalHistory a)
17 |
18 |
19 | empty : History a
20 | empty =
21 | History { past = [], future = [] }
22 |
23 |
24 | push : a -> History a -> History a
25 | push entry (History history) =
26 | History { past = entry :: history.past, future = [] }
27 |
28 |
29 | undo : a -> History a -> Maybe ( History a, a )
30 | undo current (History history) =
31 | case history.past of
32 | previous :: past ->
33 | Just
34 | ( History { past = past, future = current :: history.future }
35 | , previous
36 | )
37 |
38 | [] ->
39 | Nothing
40 |
41 |
42 | redo : a -> History a -> Maybe ( History a, a )
43 | redo current (History history) =
44 | case history.future of
45 | next :: future ->
46 | Just
47 | ( History { past = current :: history.past, future = future }
48 | , next
49 | )
50 |
51 | [] ->
52 | Nothing
53 |
--------------------------------------------------------------------------------
/src/Editor/Function.elm:
--------------------------------------------------------------------------------
1 | module Editor.Function exposing (bufferOf, cursorRight, stateOf)
2 |
3 | import Buffer exposing (Buffer)
4 | import Editor.Model exposing (InternalState)
5 | import Editor.Update exposing (Msg(..))
6 | import Position
7 |
8 |
9 | stateOf : ( InternalState, Buffer, Cmd Msg ) -> InternalState
10 | stateOf ( state, _, _ ) =
11 | state
12 |
13 |
14 | bufferOf : ( InternalState, Buffer, Cmd Msg ) -> Buffer
15 | bufferOf ( _, buffer, _ ) =
16 | buffer
17 |
18 |
19 | cursorRight state buffer =
20 | let
21 | newCursor =
22 | let
23 | moveFrom =
24 | case state.selection of
25 | Just selection ->
26 | Position.order selection state.cursor
27 | |> Tuple.second
28 |
29 | Nothing ->
30 | state.cursor
31 | in
32 | Position.nextColumn moveFrom
33 | |> Buffer.clampPosition Buffer.Forward buffer
34 |
35 | -- cmd =
36 | -- case state.cursor.line /= newCursor.line of
37 | -- True ->
38 | -- setEditorViewportForLine state.config.lineHeight newCursor.line
39 | --
40 | -- False ->
41 | -- Cmd.none
42 | in
43 | ( { state
44 | | cursor = newCursor
45 | , selection = Nothing
46 | }
47 | , buffer
48 | , Cmd.none
49 | )
50 |
--------------------------------------------------------------------------------
/demo/elm-dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "src",
5 | "../src"
6 | ],
7 | "elm-version": "0.19.1",
8 | "dependencies": {
9 | "direct": {
10 | "Janiczek/cmd-extra": "1.1.0",
11 | "elm/browser": "1.0.2",
12 | "elm/core": "1.0.4",
13 | "elm/html": "1.0.0",
14 | "elm/json": "1.1.3",
15 | "elm/parser": "1.1.0",
16 | "elm/time": "1.0.0",
17 | "elm-community/list-extra": "8.2.3",
18 | "elm-community/maybe-extra": "5.1.0",
19 | "elm-community/string-extra": "4.0.1",
20 | "folkertdev/elm-paragraph": "1.0.0",
21 | "jinjor/elm-debounce": "3.0.0",
22 | "jxxcarlson/elm-markdown": "6.0.1",
23 | "jxxcarlson/htree": "2.0.1",
24 | "lovasoa/elm-rolling-list": "1.1.4",
25 | "lukewestby/elm-string-interpolate": "1.0.4",
26 | "pablohirafuji/elm-syntax-highlight": "3.2.0",
27 | "zwilias/elm-rosetree": "1.5.0"
28 | },
29 | "indirect": {
30 | "NoRedInk/elm-string-conversions": "1.0.1",
31 | "elm/bytes": "1.0.8",
32 | "elm/file": "1.0.5",
33 | "elm/http": "2.0.0",
34 | "elm/regex": "1.0.0",
35 | "elm/url": "1.0.0",
36 | "elm/virtual-dom": "1.0.2",
37 | "zwilias/elm-html-string": "2.0.2"
38 | }
39 | },
40 | "test-dependencies": {
41 | "direct": {},
42 | "indirect": {
43 | "Janiczek/architecture-test": "2.1.0",
44 | "carwow/elm-slider": "10.0.0",
45 | "debois/elm-dom": "1.3.0",
46 | "elm/random": "1.0.0",
47 | "elm-explorations/test": "1.2.2"
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/demo/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "src"
5 | ],
6 | "elm-version": "0.19.1",
7 | "dependencies": {
8 | "direct": {
9 | "Janiczek/cmd-extra": "1.1.0",
10 | "elm/browser": "1.0.2",
11 | "elm/core": "1.0.4",
12 | "elm/html": "1.0.0",
13 | "elm/json": "1.1.3",
14 | "elm/time": "1.0.0",
15 | "jxxcarlson/elm-markdown": "6.0.1",
16 | "jxxcarlson/elm-text-editor": "7.0.8",
17 | "zwilias/elm-rosetree": "1.5.0"
18 | },
19 | "indirect": {
20 | "Janiczek/architecture-test": "2.1.0",
21 | "NoRedInk/elm-string-conversions": "1.0.1",
22 | "carwow/elm-slider": "10.0.0",
23 | "debois/elm-dom": "1.3.0",
24 | "elm/bytes": "1.0.8",
25 | "elm/file": "1.0.5",
26 | "elm/http": "2.0.0",
27 | "elm/parser": "1.1.0",
28 | "elm/random": "1.0.0",
29 | "elm/regex": "1.0.0",
30 | "elm/url": "1.0.0",
31 | "elm/virtual-dom": "1.0.2",
32 | "elm-community/list-extra": "8.2.3",
33 | "elm-community/maybe-extra": "5.1.0",
34 | "elm-community/string-extra": "4.0.1",
35 | "elm-explorations/test": "1.2.2",
36 | "folkertdev/elm-paragraph": "1.0.0",
37 | "jinjor/elm-debounce": "3.0.0",
38 | "jxxcarlson/htree": "2.0.1",
39 | "lovasoa/elm-rolling-list": "1.1.4",
40 | "lukewestby/elm-string-interpolate": "1.0.4",
41 | "pablohirafuji/elm-syntax-highlight": "3.2.0",
42 | "zwilias/elm-html-string": "2.0.2"
43 | }
44 | },
45 | "test-dependencies": {
46 | "direct": {},
47 | "indirect": {}
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/demo/elm-pub.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": [
4 | "src"
5 | ],
6 | "elm-version": "0.19.1",
7 | "dependencies": {
8 | "direct": {
9 | "Janiczek/cmd-extra": "1.1.0",
10 | "elm/browser": "1.0.2",
11 | "elm/core": "1.0.4",
12 | "elm/html": "1.0.0",
13 | "elm/json": "1.1.3",
14 | "elm/time": "1.0.0",
15 | "jxxcarlson/elm-markdown": "6.0.0",
16 | "jxxcarlson/elm-text-editor": "7.0.7",
17 | "zwilias/elm-rosetree": "1.5.0"
18 | },
19 | "indirect": {
20 | "Janiczek/architecture-test": "2.1.0",
21 | "NoRedInk/elm-string-conversions": "1.0.1",
22 | "carwow/elm-slider": "10.0.0",
23 | "debois/elm-dom": "1.3.0",
24 | "elm/bytes": "1.0.8",
25 | "elm/file": "1.0.5",
26 | "elm/http": "2.0.0",
27 | "elm/parser": "1.1.0",
28 | "elm/random": "1.0.0",
29 | "elm/regex": "1.0.0",
30 | "elm/url": "1.0.0",
31 | "elm/virtual-dom": "1.0.2",
32 | "elm-community/list-extra": "8.2.3",
33 | "elm-community/maybe-extra": "5.1.0",
34 | "elm-community/string-extra": "4.0.1",
35 | "elm-explorations/test": "1.2.2",
36 | "folkertdev/elm-paragraph": "1.0.0",
37 | "jinjor/elm-debounce": "3.0.0",
38 | "jxxcarlson/htree": "2.0.1",
39 | "lovasoa/elm-rolling-list": "1.1.4",
40 | "lukewestby/elm-string-interpolate": "1.0.4",
41 | "pablohirafuji/elm-syntax-highlight": "3.2.0",
42 | "zwilias/elm-html-string": "2.0.2"
43 | }
44 | },
45 | "test-dependencies": {
46 | "direct": {},
47 | "indirect": {}
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/testSpares/EditorTest.elm:
--------------------------------------------------------------------------------
1 | module EditorTest exposing (suite)
2 |
3 | import Expect exposing (Expectation)
4 | import Test exposing (..)
5 | import Tests.CursorLeft as CursorLeft
6 | import Tests.CursorRight as CursorRight
7 | import Tests.Insert as Insert
8 | import Tests.Invariants as Invariants
9 | import Tests.NoOp as NoOp
10 |
11 |
12 |
13 | --suite : Test
14 | --suite =
15 | -- describe "String.reverse"
16 | -- -- Nest as many descriptions as you like.
17 | -- [ test "has no effect on a palindrome" <|
18 | -- \_ ->
19 | -- let
20 | -- palindrome =
21 | -- "hannah"
22 | -- in
23 | -- Expect.equal palindrome (String.reverse palindrome)
24 | -- ]
25 | --
26 |
27 |
28 | suite : Test
29 | suite =
30 | concat
31 | [ -- describe "Invariants"
32 | -- [ describe "cursor"
33 | -- [ describe "line"
34 | -- []
35 | -- , describe "column"
36 | -- []
37 | -- ]
38 | -- ]
39 | describe "skip"
40 | [ describe "NoOp"
41 | [ NoOp.doesNothing
42 | ]
43 | , describe "Invariants"
44 | [ Invariants.cursorLineIsAlwaysPositive
45 | , Invariants.cursorColumnIsAlwaysPositive
46 | , Invariants.cursorLineNeverGetsToNonexistingLine
47 | , Invariants.cursorLineNeverGetsToBeyondEndOfLine
48 | ]
49 | , describe "Insert"
50 | [ Insert.one ]
51 | , describe
52 | "CursorLeft"
53 | [ CursorLeft.notNegativeColumn ]
54 | , describe "CursorRight"
55 | [ CursorRight.advance ]
56 | ]
57 | ]
58 |
--------------------------------------------------------------------------------
/demo/public/assets/outside.js:
--------------------------------------------------------------------------------
1 | // OUTSIDE
2 |
3 | app.ports.infoForOutside.subscribe(msg => {
4 |
5 | console.log("app.ports.infoForOutside")
6 |
7 | switch(msg.tag) {
8 |
9 | case "AskForClipBoard":
10 | console.log("AskForClipBoard")
11 |
12 | navigator.clipboard.readText()
13 | .then(text => {
14 | console.log('Clipboard (outside):', text);
15 | app.ports.infoForElm.send({tag: "GotClipboard", data: text})
16 | })
17 | .catch(err => {
18 | console.error('Failed to read clipboard: ', err);
19 | });
20 |
21 | break;
22 |
23 | case "WriteToClipboard":
24 | console.log("WriteToClipboard", JSON.stringify(msg.data))
25 |
26 | navigator.permissions.query({name: "clipboard-write"}).then(result => {
27 | if (result.state == "granted" || result.state == "prompt") {
28 | updateClipboard(JSON.stringify(msg.data))
29 | }
30 | });
31 |
32 |
33 | break;
34 |
35 | case "Highlight":
36 |
37 | console.log("Highlight", msg.data)
38 | var id = "#".concat(msg.data.id)
39 | var lastId = msg.data.lastId
40 | console.log("Highlight (id, lastId)", id, lastId)
41 |
42 | var element = document.querySelector(id)
43 | if (element != null) {
44 | element.classList.add("highlight")
45 | } else {
46 | console.log("Add: could not find id", id)
47 | }
48 |
49 | var lastElement = document.querySelector(lastId)
50 | if (lastElement != null) {
51 | lastElement.classList.remove("highlight")
52 | } else {
53 | console.log("Remove: could not find last id",lastId)
54 | }
55 |
56 | break;
57 |
58 | }
59 |
60 | function updateClipboard(newClip) {
61 | navigator.clipboard.writeText(newClip).then(function() {
62 | console.log("Wrote to clipboard");
63 | }, function() {
64 | console.log ("Clipboard write failed");
65 | });
66 | }
67 |
68 | })
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/Editor/Config.elm:
--------------------------------------------------------------------------------
1 | module Editor.Config exposing (Config, WrapOption(..), WrapParams, default, setMaximumWrapWidth, setOptimumWrapWidth, setWrapOption)
2 |
3 | {-| Use this module to configure the editor.
4 | The `default` value is a basic configuration
5 | which you can modify like this:
6 |
7 | config =
8 | { default | lines = 30 }
9 |
10 | @docs Config, WrapOption, WrapParams, default, setMaximumWrapWidth, setOptimumWrapWidth, setWrapOption
11 |
12 | -}
13 |
14 |
15 | {-| -}
16 | type alias Config =
17 | { wrapParams : WrapParams
18 | , showInfoPanel : Bool
19 | , wrapOption : WrapOption
20 | , width : Float
21 | , height : Float
22 | , lineHeight : Float
23 | , fontProportion : Float
24 | , lineHeightFactor : Float
25 | }
26 |
27 |
28 | {-| -}
29 | type alias WrapParams =
30 | { maximumWidth : Int
31 | , optimalWidth : Int
32 | , stringWidth : String -> Int
33 | }
34 |
35 |
36 | {-| -}
37 | type WrapOption
38 | = DoWrap
39 | | DontWrap
40 |
41 |
42 | {-| -}
43 | default : Config
44 | default =
45 | { wrapParams = { maximumWidth = 50, optimalWidth = 45, stringWidth = String.length }
46 | , showInfoPanel = False
47 | , wrapOption = DoWrap
48 | , width = 400
49 | , height = 400
50 | , lineHeight = 18
51 | , fontProportion = 0.7
52 | , lineHeightFactor = 1.0
53 | }
54 |
55 |
56 |
57 | -- TODO: Make maximumWidth and optimalWidth configurable at startup and at runtime
58 |
59 |
60 | {-| -}
61 | setWrapOption : WrapOption -> Config -> Config
62 | setWrapOption wrapOption config =
63 | { config | wrapOption = wrapOption }
64 |
65 |
66 | {-| -}
67 | setMaximumWrapWidth : Int -> Config -> Config
68 | setMaximumWrapWidth k config =
69 | let
70 | w =
71 | config.wrapParams
72 |
73 | newWrapParams =
74 | { w | maximumWidth = k }
75 | in
76 | { config | wrapParams = newWrapParams }
77 |
78 |
79 | {-| -}
80 | setOptimumWrapWidth : Int -> Config -> Config
81 | setOptimumWrapWidth k config =
82 | let
83 | w =
84 | config.wrapParams
85 |
86 | newWrapParams =
87 | { w | optimalWidth = k }
88 | in
89 | { config | wrapParams = newWrapParams }
90 |
--------------------------------------------------------------------------------
/demo/src/Outside.elm:
--------------------------------------------------------------------------------
1 | port module Outside exposing
2 | ( InfoForElm(..)
3 | , InfoForOutside(..)
4 | , getInfo
5 | , sendInfo
6 | )
7 |
8 | {-| This module manages all interactions with the external JS-world.
9 | At the moment, there is just one: external copy-paste.
10 | -}
11 |
12 | import Json.Decode as D
13 | import Json.Encode as E
14 |
15 |
16 | port infoForOutside : GenericOutsideData -> Cmd msg
17 |
18 |
19 | port infoForElm : (GenericOutsideData -> msg) -> Sub msg
20 |
21 |
22 | type alias GenericOutsideData =
23 | { tag : String, data : E.Value }
24 |
25 |
26 | type InfoForElm
27 | = GotClipboard String
28 |
29 |
30 | type InfoForOutside
31 | = AskForClipBoard E.Value
32 | | WriteToClipBoard String
33 | | Highlight ( Maybe String, String )
34 |
35 |
36 | getInfo : (InfoForElm -> msg) -> (String -> msg) -> Sub msg
37 | getInfo tagger onError =
38 | infoForElm
39 | (\outsideInfo ->
40 | case outsideInfo.tag of
41 | "GotClipboard" ->
42 | case D.decodeValue clipboardDecoder outsideInfo.data of
43 | Ok result ->
44 | tagger <| GotClipboard result
45 |
46 | Err e ->
47 | onError <| ""
48 |
49 | _ ->
50 | onError <| "Unexpected info from outside"
51 | )
52 |
53 |
54 | sendInfo : InfoForOutside -> Cmd msg
55 | sendInfo info =
56 | case info of
57 | AskForClipBoard value ->
58 | infoForOutside { tag = "AskForClipBoard", data = E.null }
59 |
60 | WriteToClipBoard str ->
61 | infoForOutside { tag = "WriteToClipboard", data = E.string str }
62 |
63 | Highlight idPair ->
64 | infoForOutside { tag = "Highlight", data = encodeSelectedIdData idPair }
65 |
66 |
67 | encodeSelectedIdData : ( Maybe String, String ) -> E.Value
68 | encodeSelectedIdData ( maybeLastId, id ) =
69 | E.object
70 | [ ( "lastId", E.string (maybeLastId |> Maybe.withDefault "nonexistent") )
71 | , ( "id", E.string id )
72 | ]
73 |
74 |
75 |
76 | -- DECODERS --
77 |
78 |
79 | clipboardDecoder : D.Decoder String
80 | clipboardDecoder =
81 | -- D.field "data" D.string
82 | D.string
83 |
--------------------------------------------------------------------------------
/testSpares/Tests/CursorLeft.elm:
--------------------------------------------------------------------------------
1 | module Tests.CursorLeft exposing (notNegativeColumn)
2 |
3 | import ArchitectureTest exposing (..)
4 | import Buffer
5 | import Editor exposing (..)
6 | import Expect exposing (Expectation)
7 | import Fuzz exposing (Fuzzer)
8 | import Test exposing (..)
9 | import Tests.Common exposing (..)
10 |
11 |
12 | retreat : Test
13 | retreat =
14 | msgTest "CursorLeft moves the cursor back one unit" app cursorLeft <|
15 | \initialModel cursorLeft finalModel ->
16 | let
17 | initialCursor =
18 | Editor.getCursor initialModel
19 |
20 | finalCursor =
21 | Editor.getCursor finalModel
22 |
23 | finallLineLength =
24 | Buffer.lineEnd initialCursor.line (Editor.getBuffer initialModel)
25 |
26 | cursorWasAtEndOfLine =
27 | -- ask if the initial cursor was at the end of the line
28 | case Maybe.map2 (-) finallLineLength (Just finalCursor.column) of
29 | Just 0 ->
30 | True
31 |
32 | _ ->
33 | False
34 |
35 | ok =
36 | case ( finalCursor.line - initialCursor.line, initialCursor.column - finalCursor.column, cursorWasAtEndOfLine ) of
37 | ( 0, 1, _ ) ->
38 | -- move back in same line
39 | True
40 |
41 | ( 1, _, True ) ->
42 | -- move back to end of previous line
43 | True
44 |
45 | ( 0, 0, _ ) ->
46 | -- cursor was at beginning of text: can't move any more
47 | True
48 |
49 | _ ->
50 | False
51 | in
52 | ok
53 | |> Expect.equal True
54 |
55 |
56 | notNegativeColumn : Test
57 | notNegativeColumn =
58 | msgTest "CursorLeft never results in negative cursor columns" app cursorLeft <|
59 | \_ _ finalModel ->
60 | let
61 | finalCursor =
62 | Editor.getCursor finalModel
63 | in
64 | finalCursor.column
65 | |> Expect.greaterThan -1
66 |
--------------------------------------------------------------------------------
/docs/README_SYDNEY_NEMZER.md:
--------------------------------------------------------------------------------
1 | # elm-text-editor
2 |
3 | A flexible text editor written in Elm
4 |
5 | [Check out the demo](https://sidneynemzer.github.io/elm-text-editor/)
6 |
7 | > Note:
8 | > * This project is not published yet on package.elm-lang.org
9 | > * I would not consider this project ready for use in production, since it's missing major features like scrolling and line wrap
10 |
11 | ## Features / Architecture
12 |
13 | This library implements an editor (duh) and a buffer. The buffer is separate from the editor to allow multiple editors to use the same buffer, such as in a multi-panel text editor.
14 |
15 | > Note: There's a checkmark next to implemented features
16 |
17 | ### Buffer
18 |
19 | - [x] file content
20 | - [ ] save status
21 | - [ ] syntax highlighting (cached)
22 | - [ ] decorations (eg underlines, tooltips, gutter icons)
23 |
24 | The buffer implementation has helper functions for manipulating its content, like finding the end of a word.
25 |
26 | ### Editor
27 |
28 | - [x] cursor location
29 | - [x] selection
30 | - [x] rendering to the DOM
31 | - [x] UI interaction (mouse and keyboard)
32 | - [x] undo history
33 | - [ ] scroll position
34 | - [ ] auto-complete dialog
35 | - [ ] open decorations (in other words, decorations exist in the buffer but each editor tracks open decorations)
36 | - [ ] line wrap
37 |
38 | I hope that one day this project will be comparable to Ace and CodeMirror, but Ace and CodeMirror have had quite a head start (about 7 years!).
39 |
40 | ## Previous work and Inspiration
41 |
42 | The editor behavior and keyboard shortcuts are based on [Atom][] (because that's the editor I'm most familiar with).
43 |
44 | [Ace][] and [CodeMirror][] are text editors designed to work in a web browser. They're both written in JavaScript, so integration with Elm is pretty meh.
45 |
46 | [Janiczek][] recently demonstrated [a text editor in pure elm][Janiczek-editor-discourse], which implements work-arounds for several issues I had faced in the past when creating a pure elm editor.
47 |
48 | > I hope this inspires somebody to try some stuff in Elm they’ve been needing but seemed too big / hard for them! You might, like me with this project, find out it’s in your reach - no doubt thanks to Elm
49 | > *- Janiczek*
50 |
51 | [Atom]: https://atom.io
52 | [Ace]: https://ace.c9.io
53 | [CodeMirror]: https://codemirror.net
54 | [Janiczek]: https://github.com/Janiczek
55 | [Janiczek-editor-discourse]: https://discourse.elm-lang.org/t/text-editor-done-in-pure-elm/1365
56 |
--------------------------------------------------------------------------------
/src/Position.elm:
--------------------------------------------------------------------------------
1 | module Position exposing
2 | ( Position
3 | , addColumn
4 | , addLine
5 | , between
6 | , nextColumn
7 | , nextLine
8 | , order
9 | , previousColumn
10 | , previousLine
11 | , setColumn
12 | , shift
13 | )
14 |
15 |
16 | type alias Position =
17 | { line : Int, column : Int }
18 |
19 |
20 | order : Position -> Position -> ( Position, Position )
21 | order pos1 pos2 =
22 | if pos2.line > pos1.line then
23 | ( pos1, pos2 )
24 |
25 | else if pos2.line == pos1.line && pos2.column > pos1.column then
26 | ( pos1, pos2 )
27 |
28 | else
29 | ( pos2, pos1 )
30 |
31 |
32 | betweenHelp : Int -> Int -> Int -> Bool
33 | betweenHelp start end point =
34 | if start > end then
35 | betweenHelp end start point
36 |
37 | else
38 | start /= end && point >= start && point < end
39 |
40 |
41 | between : Position -> Position -> Position -> Bool
42 | between pos1 pos2 { line, column } =
43 | let
44 | ( start, end ) =
45 | order pos1 pos2
46 | in
47 | if start.line == end.line then
48 | line == start.line && betweenHelp start.column end.column column
49 |
50 | else if start.line == line then
51 | column >= start.column
52 |
53 | else if end.line == line then
54 | column < end.column
55 |
56 | else
57 | betweenHelp start.line end.line line
58 |
59 |
60 | addColumn : Int -> Position -> Position
61 | addColumn amount position =
62 | { position | column = position.column + amount }
63 |
64 |
65 | nextColumn : Position -> Position
66 | nextColumn =
67 | addColumn 1
68 |
69 |
70 | previousColumn : Position -> Position
71 | previousColumn =
72 | addColumn -1
73 |
74 |
75 | setColumn : Int -> Position -> Position
76 | setColumn column position =
77 | { position | column = column }
78 |
79 |
80 | addLine : Int -> Position -> Position
81 | addLine amount position =
82 | { position | line = position.line + amount }
83 |
84 |
85 | nextLine : Position -> Position
86 | nextLine =
87 | addLine 1
88 |
89 |
90 | previousLine : Position -> Position
91 | previousLine =
92 | addLine -1
93 |
94 |
95 | shift : Int -> Position -> Position
96 | shift k position =
97 | case position.line + k >= 0 of
98 | True ->
99 | { position | line = position.line + k }
100 |
101 | False ->
102 | { position | line = 0 }
103 |
--------------------------------------------------------------------------------
/testSpares/Tests/Invariants.elm:
--------------------------------------------------------------------------------
1 | module Tests.Invariants exposing
2 | ( cursorColumnIsAlwaysPositive
3 | , cursorLineIsAlwaysPositive
4 | , cursorLineNeverGetsToBeyondEndOfLine
5 | , cursorLineNeverGetsToNonexistingLine
6 | )
7 |
8 | import ArchitectureTest exposing (..)
9 | import Buffer
10 | import Editor exposing (..)
11 | import Expect exposing (Expectation)
12 | import List.Extra
13 | import Test exposing (..)
14 | import Tests.Common exposing (..)
15 |
16 |
17 | cursorLineIsAlwaysPositive : Test
18 | cursorLineIsAlwaysPositive =
19 | invariantTest "cursor.line is always positive" app <|
20 | \_ _ finalModel ->
21 | let
22 | cursor =
23 | Editor.getCursor finalModel
24 | in
25 | cursor.line
26 | |> Expect.atLeast 0
27 |
28 |
29 | cursorColumnIsAlwaysPositive : Test
30 | cursorColumnIsAlwaysPositive =
31 | invariantTest "cursor.column is always positive" app <|
32 | \_ _ finalModel ->
33 | let
34 | cursor =
35 | Editor.getCursor finalModel
36 | in
37 | cursor.column
38 | |> Expect.atLeast 0
39 |
40 |
41 | cursorLineNeverGetsToNonexistingLine : Test
42 | cursorLineNeverGetsToNonexistingLine =
43 | invariantTest "cursor.line never gets to nonexisting line" app <|
44 | \_ _ finalModel ->
45 | let
46 | cursor =
47 | Editor.getCursor finalModel
48 |
49 | lastLine =
50 | Editor.getBuffer finalModel
51 | |> Buffer.lastPosition
52 | |> .line
53 | in
54 | cursor.line
55 | |> Expect.atMost lastLine
56 |
57 |
58 | cursorLineNeverGetsToBeyondEndOfLine : Test
59 | cursorLineNeverGetsToBeyondEndOfLine =
60 | invariantTest "cursor.line never gets beyond end of line" app <|
61 | \_ _ finalModel ->
62 | let
63 | cursor =
64 | Editor.getCursor finalModel
65 |
66 | currentLineLength : Int
67 | currentLineLength =
68 | Editor.getBuffer finalModel
69 | |> Buffer.lines
70 | |> List.Extra.getAt cursor.line
71 | |> Maybe.map String.length
72 | |> Maybe.withDefault 1000000000
73 | in
74 | cursor.column
75 | |> Expect.atMost currentLineLength
76 |
--------------------------------------------------------------------------------
/src/Editor/SearchX.elm:
--------------------------------------------------------------------------------
1 | module Editor.SearchX exposing (search)
2 |
3 | import Buffer exposing (Buffer, lines)
4 | import Position exposing (Position)
5 |
6 |
7 | {-| Return a list of pairs (k, s), where s
8 | -}
9 |
10 |
11 |
12 | -- searchHits : String -> String -> List (Position, Position)
13 |
14 |
15 | search : String -> Buffer -> List ( Position, Position )
16 | search key buffer =
17 | searchHits key (lines buffer)
18 |
19 |
20 | {-|
21 |
22 | searchHits "AB" ["about this, we know", "that Babs is the best in the lab", "a stitch in time saves nine"]
23 | --> [({ column = 0, line = 0 },{ column = 5, line = 0 }),({ column = 29, line = 1 },{ column = 32, line = 1 })]
24 |
25 | -}
26 | searchHits : String -> List String -> List ( Position, Position )
27 | searchHits key lines_ =
28 | let
29 | key_ =
30 | key
31 | in
32 | indexedFilterMap (\i line -> String.contains key_ line) lines_
33 | |> List.map (\( idx, str ) -> ( idx, str, matches key_ str ))
34 | |> List.map positions
35 | |> List.concat
36 |
37 |
38 | {-|
39 |
40 | positions (5, "This is about our lab.", ["about", "lab"])
41 | --> [({ column = 8, line = 5 },{ column = 13, line = 5 }),({ column = 18, line = 5 },{ column = 21, line = 5 })]
42 |
43 | -}
44 | positions : ( Int, String, List String ) -> List ( Position, Position )
45 | positions ( line, source, hits ) =
46 | List.map (\hit -> stringIndices hit source) hits
47 | |> List.concat
48 | |> List.map (\( start, end ) -> ( Position line start, Position line end ))
49 |
50 |
51 | {-|
52 |
53 | stringIndices "ab" "This is about our lab."
54 | --> [(8,10),(19,21)] : List ( Int, Int )
55 |
56 | -}
57 | stringIndices : String -> String -> List ( Int, Int )
58 | stringIndices key source =
59 | let
60 | n =
61 | String.length key
62 | in
63 | String.indices key source
64 | |> List.map (\i -> ( i, i + n ))
65 |
66 |
67 | {-|
68 |
69 | matches "ab" "abc, hoorAy, yada, Blab about it"
70 | ["abc,","blab","about"] : List String
71 |
72 | -}
73 | matches : String -> String -> List String
74 | matches key str =
75 | str
76 | |> String.words
77 | |> List.filter (\word -> String.contains key word)
78 |
79 |
80 | {-|
81 |
82 | indexedFilterMap (\i x -> i >= 1 && i <= 3) [0,1,2,3,4,5,6]
83 | --> [(1,1),(2,2),(3,3)]
84 |
85 | -}
86 | indexedFilterMap : (Int -> a -> Bool) -> List a -> List ( Int, a )
87 | indexedFilterMap filter list =
88 | list
89 | |> List.indexedMap (\k item -> ( k, item ))
90 | |> List.filter (\( i, item ) -> filter i item)
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # A Pure Elm Text Editor
2 |
3 |
4 | This project, a text editor written in pure Elm, is a fork of
5 | [work by Sydney Nemzer](https://github.com/SidneyNemzer/elm-text-editor).
6 | His [demo](https://sidneynemzer.github.io/elm-text-editor/),
7 | inspired by prior work of Martin Janiczek, shows the
8 | feasibility of writing such a text editor and establishes an elegant and powerful foundation for future work. Many kudos to Sydney.
9 |
10 |
11 | This
12 | [forked repo](https://github.com/jxxcarlson/elm-text-editor) adds
13 | scrolling, copy, cut, and paste, search and replace, text wrap,
14 | and an API for embedding the editor in another app.
15 | Here are some examples which make use of the present library.
16 |
17 | - [Demo-simple](https://jxxcarlson.github.io/app/editor-simple/index.html)
18 |
19 | - [Demo](https://jxxcarlson.github.io/app/editor/index.html) This one
20 | implements external copy-paste: copy something somewhere with Cmd-C or
21 | whatever, then use ctrl-shift-V to paste it in the editor.
22 | At the moment, external copy-paste only works in Chrome.
23 |
24 | - [Markdown Example](https://markdown.minilatex.app/) The editor is
25 | hosted by an app that renders text written Markdown. The text may
26 | include mathematics written in TeX/LaTeX. The app features
27 | left-to-right sync: doing ctrl-shift-S in the editor window
28 | (Left) synchronizes the source text with the rendered text (Right).
29 | Still to do: right-to-left sync. And much more.
30 |
31 | ## Running the examples
32 |
33 | ```bash
34 | npm install
35 |
36 | npm start
37 | ```
38 |
39 |
40 | ## Embedding the Editor
41 |
42 | - Use the `demo-simple` and `demo` apps of this repo as models, or consult
43 | the [Markdown Example Code](https://github.com/jxxcarlson/elm-markdown/tree/master/app-demo-fancy)
44 | - In order to implement external copy-paste (ctrl-shift V),
45 | the [Demo](https://jxxcarlson.github.io/app/editor/index.html) and
46 | [Markdown Example](https://markdown.minilatex.app/) apps import a module `Outside` into `Main`. This module
47 | uses ports and references `outside.js` in `index.html`
48 |
49 |
50 | ## Plans
51 |
52 | - I would very much like this to be a community project; it is a tool that many of us can use to good end. I've posted some issues on the repo, and welcome comments, pull requests, and more issues.
53 |
54 |
55 | - I may post a Road Map later, but [Sydney Nemzer's README](https://github.com/SidneyNemzer/elm-text-editor/blob/master/README.md) is an excellent place to begin.
56 |
57 |
58 | ## Credits
59 |
60 | I would like to thank Folkert de Vries for help
61 | improving the performance of the editor.
62 |
63 |
64 |
--------------------------------------------------------------------------------
/src/BufferHelper.elm:
--------------------------------------------------------------------------------
1 | module BufferHelper exposing (Direction(..), Group(..), charsAround, indexFromPosition, isNonWordChar, isWhitespace, isWordChar, slice, stringCharAt, tuple3CharsPred, tuple3MapAll)
2 |
3 | import Array exposing (Array)
4 | import List.Extra
5 | import Maybe.Extra
6 | import Position exposing (Position)
7 | import String.Extra
8 | import Util.Array
9 |
10 |
11 | stringCharAt : Int -> String -> Maybe Char
12 | stringCharAt index string =
13 | String.slice index (index + 1) string
14 | |> String.uncons
15 | |> Maybe.map Tuple.first
16 |
17 |
18 | charsAround : Int -> String -> ( Maybe Char, Maybe Char, Maybe Char )
19 | charsAround index string =
20 | ( stringCharAt (index - 1) string
21 | , stringCharAt index string
22 | , stringCharAt (index + 1) string
23 | )
24 |
25 |
26 | tuple3MapAll : (a -> b) -> ( a, a, a ) -> ( b, b, b )
27 | tuple3MapAll fn ( a1, a2, a3 ) =
28 | ( fn a1, fn a2, fn a3 )
29 |
30 |
31 | tuple3CharsPred :
32 | (Char -> Bool)
33 | -> ( Maybe Char, Maybe Char, Maybe Char )
34 | -> ( Bool, Bool, Bool )
35 | tuple3CharsPred pred =
36 | tuple3MapAll (Maybe.map pred >> Maybe.withDefault False)
37 |
38 |
39 | {-| Internal function for getting the index of the position in a string
40 |
41 | indexFromPosition "reddish\ngreen" (Position 0 2)
42 | --> Just 2
43 |
44 | indexFromPosition "reddish\ngreen" (Position 1 2)
45 | --> Just 10
46 |
47 | -}
48 | indexFromPosition : String -> Position -> Maybe Int
49 | indexFromPosition str position =
50 | -- Doesn't validate columns, only lines
51 | if position.line == 0 then
52 | Just position.column
53 |
54 | else
55 | String.indexes "\n" str
56 | |> List.Extra.getAt (position.line - 1)
57 | |> Maybe.map (\line -> line + position.column + 1)
58 |
59 |
60 | slice : Position -> Position -> String -> Maybe String
61 | slice pos1 pos2 str =
62 | let
63 | index1 =
64 | indexFromPosition str pos1
65 |
66 | index2 =
67 | indexFromPosition str pos2
68 | in
69 | case ( index1, index2 ) of
70 | ( Just i, Just j ) ->
71 | String.slice i j str |> Just
72 |
73 | ( _, _ ) ->
74 | Nothing
75 |
76 |
77 |
78 | -- GROUPING
79 |
80 |
81 | isWhitespace : Char -> Bool
82 | isWhitespace =
83 | String.fromChar >> String.trim >> (==) ""
84 |
85 |
86 | isNonWordChar : Char -> Bool
87 | isNonWordChar =
88 | String.fromChar >> (\a -> String.contains a "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…")
89 |
90 |
91 | isWordChar : Char -> Bool
92 | isWordChar char =
93 | not (isNonWordChar char) && not (isWhitespace char)
94 |
95 |
96 | type Group
97 | = None
98 | | Word
99 | | NonWord
100 |
101 |
102 | type Direction
103 | = Forward
104 | | Backward
105 |
--------------------------------------------------------------------------------
/src/Editor/Widget.elm:
--------------------------------------------------------------------------------
1 | module Editor.Widget exposing
2 | ( columnButton
3 | , columnButtonStyle
4 | , headingStyle
5 | , lightColumnButton
6 | , lightRowButton
7 | , rowButton
8 | , textField
9 | )
10 |
11 | import Editor.Style as Style
12 | import Html exposing (Attribute, Html, button, div, input, text)
13 | import Html.Attributes exposing (placeholder, style, type_)
14 | import Html.Events exposing (onClick, onInput)
15 |
16 |
17 | columnButtonStyle =
18 | [ style "margin-top" "10px"
19 | , style "font-size" "12px"
20 | , style "border" "none"
21 | , style "margin-right" "8px"
22 | ]
23 |
24 |
25 | headingStyle =
26 | [ style "font-size" "14px"
27 | , style "margin-right" "12px"
28 | , style "float" "left"
29 | ]
30 |
31 |
32 | rowButtonStyle =
33 | [ style "font-size" "12px"
34 | , style "border" "none"
35 | , style "margin-right" "8px"
36 | , style "float" "left"
37 | ]
38 |
39 |
40 | buttonLabelStyle width =
41 | [ style "font-size" "12px"
42 | , style "background-color" Style.darkGray
43 | , style "color" "#eee"
44 | , style "width" (String.fromInt width ++ "px")
45 | , style "height" "24px"
46 | , style "border" "none"
47 | , style "text-align" "left"
48 | ]
49 |
50 |
51 | lightButtonLabelStyle width =
52 | [ style "font-size" "12px"
53 | , style "color" "#444"
54 | , style "width" (String.fromInt width ++ "px")
55 | , style "height" "24px"
56 | , style "border" "none"
57 | , style "text-align" "left"
58 | ]
59 |
60 |
61 | rowButtonLabelStyle width =
62 | [ style "font-size" "12px"
63 | , style "background-color" Style.darkGray
64 | , style "color" "#eee"
65 | , style "width" (String.fromInt width ++ "px")
66 | , style "height" "24px"
67 | , style "border" "none"
68 | ]
69 |
70 |
71 | columnButton width msg str attr =
72 | div (columnButtonStyle ++ attr)
73 | [ button ([ onClick msg ] ++ buttonLabelStyle width) [ text str ] ]
74 |
75 |
76 | lightColumnButton width msg str attr =
77 | div (columnButtonStyle ++ attr)
78 | [ button ([ onClick msg ] ++ lightButtonLabelStyle width) [ text str ] ]
79 |
80 |
81 | lightRowButton width msg str attr =
82 | div (rowButtonStyle ++ attr)
83 | [ button ([ onClick msg ] ++ lightButtonLabelStyle width) [ text str ] ]
84 |
85 |
86 | rowButton width msg str attr =
87 | div (rowButtonStyle ++ attr)
88 | [ button ([ onClick msg ] ++ rowButtonLabelStyle width) [ text str ] ]
89 |
90 |
91 | textField width msg str attr innerAttr =
92 | div ([ style "margin-bottom" "10px" ] ++ attr)
93 | [ input
94 | ([ style "height" "18px"
95 | , style "width" (String.fromInt width ++ "px")
96 | , type_ "text"
97 | , placeholder str
98 | , style "margin-right" "8px"
99 | , onInput msg
100 | ]
101 | ++ innerAttr
102 | )
103 | []
104 | ]
105 |
--------------------------------------------------------------------------------
/testSpares/Tests/CursorRight.elm:
--------------------------------------------------------------------------------
1 | module Tests.CursorRight exposing (advance)
2 |
3 | import ArchitectureTest exposing (..)
4 | import Buffer
5 | import Editor
6 | import Expect exposing (Expectation)
7 | import Fuzz exposing (Fuzzer)
8 | import Test exposing (..)
9 | import Tests.Common exposing (..)
10 |
11 |
12 | advance : Test
13 | advance =
14 | msgTest "CursorRight advances the cursor" app cursorRight <|
15 | \initialModel _ finalModel ->
16 | let
17 | initialCursor =
18 | Editor.getCursor initialModel
19 |
20 | finalCursor =
21 | Editor.getCursor finalModel
22 |
23 | ok =
24 | case ( finalCursor.line - initialCursor.line, finalCursor.column - initialCursor.column, finalCursor.column ) of
25 | ( 0, 1, _ ) ->
26 | -- advances cursor by 1 but does not change line
27 | True
28 |
29 | ( 1, _, 0 ) ->
30 | -- moves to beginning of next line (WEAK)
31 | True
32 |
33 | ( 0, _, _ ) ->
34 | False
35 |
36 | _ ->
37 | False
38 | in
39 | ok
40 | |> Expect.equal True
41 |
42 |
43 |
44 | --
45 | --doesNotAdvanceBeyondEndOfLine : Test
46 | --doesNotAdvanceBeyondEndOfLine =
47 | -- msgTest "CursorRight doesn't advance the cursor beyond the end of the line " app cursorRight <|
48 | -- \initialModel _ finalModel ->
49 | -- let
50 | -- initialCursor =
51 | -- Editor.getCursor initialModel
52 | --
53 | -- finalCursor =
54 | -- Editor.getCursor finalModel
55 | --
56 | -- initialLineLength =
57 | -- Buffer.lineEnd initialCursor.line (Editor.getBuffer initialModel)
58 | --
59 | -- cursorWasAtEndOfLine =
60 | -- -- ask if the initial cursor was at the end of the line
61 | -- case Maybe.map2 (-) initialLineLength (Just initialCursor.column) of
62 | -- Just 0 ->
63 | -- True
64 | --
65 | -- _ ->
66 | -- False
67 | --
68 | -- ok =
69 | -- case ( finalCursor.line - initialCursor.line, finalCursor.column - initialCursor.column, cursorWasAtEndOfLine ) of
70 | -- ( 0, 1, _ ) ->
71 | -- -- advances cursor by 1 but does not change line
72 | -- True
73 | --
74 | -- ( 1, _, True ) ->
75 | -- -- moves to beginning of next line (WEAK)
76 | -- True
77 | --
78 | -- ( 0, _, _ ) ->
79 | -- False
80 | --
81 | -- _ ->
82 | -- False
83 | -- in
84 | -- ok
85 | -- |> Expect.equal True
86 |
--------------------------------------------------------------------------------
/src/Editor/Search.elm:
--------------------------------------------------------------------------------
1 | module Editor.Search exposing (search)
2 |
3 | import Buffer exposing (Buffer, lines)
4 | import Position exposing (Position)
5 |
6 |
7 | {-| Return a list of pairs (k, s), where s
8 | -}
9 |
10 |
11 |
12 | -- searchHits : String -> String -> List (Position, Position)
13 |
14 |
15 | search : String -> Buffer -> List ( Position, Position )
16 | search key buffer =
17 | searchHits key (lines buffer)
18 |
19 |
20 | {-|
21 |
22 | searchHits "AB" ["about this, we know", "that Babs is the best in the lab", "a stitch in time saves nine"]
23 | --> [({ column = 0, line = 0 },{ column = 5, line = 0 }),({ column = 29, line = 1 },{ column = 32, line = 1 })]
24 |
25 | -}
26 |
27 |
28 |
29 | -- searchHits : String -> List String -> List ( Position, Position )
30 |
31 |
32 | searchHits key lines_ =
33 | let
34 | width =
35 | String.length key
36 |
37 | expand : ( Int, List Int ) -> List ( Int, Int )
38 | expand ( i, list ) =
39 | list |> List.map (\item -> ( i, item ))
40 |
41 | makePositions : ( Int, Int ) -> ( Position, Position )
42 | makePositions ( line, column ) =
43 | ( Position line column, Position line (column + width) )
44 | in
45 | List.indexedMap (\i line -> ( i, String.indexes key line )) lines_
46 | |> List.map expand
47 | |> List.concat
48 | |> List.map makePositions
49 |
50 |
51 |
52 | --
53 | --
54 | --{-|
55 | --
56 | -- positions (5, "This is about our lab.", ["about", "lab"])
57 | -- --> [({ column = 8, line = 5 },{ column = 13, line = 5 }),({ column = 18, line = 5 },{ column = 21, line = 5 })]
58 | --
59 | ---}
60 | --positions : ( Int, String, List String ) -> List ( Position, Position )
61 | --positions ( line, source, hits ) =
62 | -- List.map (\hit -> stringIndices hit source) hits
63 | -- |> List.concat
64 | -- |> List.map (\( start, end ) -> ( Position line start, Position line end ))
65 | --
66 | --
67 | --{-|
68 | --
69 | -- stringIndices "ab" "This is about our lab."
70 | -- --> [(8,10),(19,21)] : List ( Int, Int )
71 | --
72 | ---}
73 | --stringIndices : String -> String -> List ( Int, Int )
74 | --stringIndices key source =
75 | -- let
76 | -- n =
77 | -- String.length key
78 | -- in
79 | -- String.indices key source
80 | -- |> List.map (\i -> ( i, i + n ))
81 | --
82 | --
83 | --{-|
84 | --
85 | -- matches "ab" "abc, hoorAy, yada, Blab about it"
86 | -- ["abc,","blab","about"] : List String
87 | --
88 | ---}
89 | --matches : String -> String -> List Int
90 | --matches key str =
91 | -- str
92 | -- |> String.words
93 | -- |> List.filter (\word -> key == word)
94 | --
95 | --
96 | --{-|
97 | --
98 | -- indexedFilterMap (\i x -> i >= 1 && i <= 3) [0,1,2,3,4,5,6]
99 | -- --> [(1,1),(2,2),(3,3)]
100 | --
101 | ---}
102 | --indexedFilterMap : (Int -> a -> Bool) -> List a -> List ( Int, a )
103 | --indexedFilterMap filter list =
104 | -- list
105 | -- |> List.indexedMap (\k item -> ( k, item ))
106 | -- |> List.filter (\( i, item ) -> filter i item)
107 |
--------------------------------------------------------------------------------
/src/Window.elm:
--------------------------------------------------------------------------------
1 | module Window exposing
2 | ( Window
3 | , getOffset
4 | , identity
5 | , identity_
6 | , scroll
7 | , scrollToIncludeCursor
8 | , select
9 | , shift
10 | , shiftPosition
11 | , shiftPosition_
12 | , shiftPosition__
13 | )
14 |
15 | import Position exposing (Position)
16 |
17 |
18 | type alias Window =
19 | { first : Int, last : Int }
20 |
21 |
22 | select : Window -> List String -> List String
23 | select window strings =
24 | strings
25 | |> indexedFilterMap (\i x -> i >= window.first && i <= window.last)
26 |
27 |
28 | {-|
29 |
30 | indexedFilterMap (\i x -> i >= 1 && i <= 3) [0,1,2,3,4,5,6]
31 | --> [1,2,3] : List number
32 |
33 | -}
34 | indexedFilterMap : (Int -> a -> Bool) -> List a -> List a
35 | indexedFilterMap filter list =
36 | list
37 | |> List.indexedMap (\k item -> ( k, item ))
38 | |> List.filter (\( i, item ) -> filter i item)
39 | |> List.map Tuple.second
40 |
41 |
42 | {-|
43 |
44 | Offset is <= 0
45 |
46 | -}
47 | getOffset : Window -> Int -> Int
48 | getOffset window lineNumber_ =
49 | min (window.last - window.first - lineNumber_) 0
50 |
51 |
52 | shiftPosition : Window -> Int -> Int -> Position
53 | shiftPosition window line column =
54 | { line = line + window.first, column = column }
55 |
56 |
57 | shiftPosition_ : Window -> Position -> Position
58 | shiftPosition_ window pos =
59 | { line = pos.line + window.first, column = pos.column }
60 |
61 |
62 | shiftPosition__ : Window -> Position -> Position
63 | shiftPosition__ window pos =
64 | let
65 | l =
66 | pos.line - window.first
67 | in
68 | if l < 0 then
69 | pos
70 |
71 | else
72 | { line = pos.line - window.first, column = pos.column }
73 |
74 |
75 | identity : Window -> Int -> Int -> Position
76 | identity window line column =
77 | { line = line, column = column }
78 |
79 |
80 | identity_ : Window -> Position -> Position
81 | identity_ window pos =
82 | pos
83 |
84 |
85 | shift : Int -> Window -> Window
86 | shift k w =
87 | { w | first = w.first + k, last = w.last + k }
88 |
89 |
90 | scroll : Int -> Window -> Window
91 | scroll k window =
92 | let
93 | index =
94 | window.first + k
95 | in
96 | case ( index < 0, index >= window.first && index <= window.last ) of
97 | ( True, _ ) ->
98 | window
99 |
100 | ( False, True ) ->
101 | window
102 |
103 | ( False, False ) ->
104 | { window | first = window.first + k, last = window.last + k }
105 |
106 |
107 | scrollToIncludeCursor : Position -> Window -> Window
108 | scrollToIncludeCursor cursor window =
109 | let
110 | line =
111 | cursor.line
112 |
113 | offset =
114 | if line >= window.last then
115 | line - window.last
116 |
117 | else if line <= window.first then
118 | line - window.first
119 |
120 | else
121 | 0
122 | in
123 | { window | first = window.first + offset, last = window.last + offset }
124 |
--------------------------------------------------------------------------------
/tests/Buffer2Test.elm:
--------------------------------------------------------------------------------
1 | module Buffer2Test exposing (suite)
2 |
3 | import Array
4 | import Array.Util
5 | import Buffer2 as Buffer exposing (Buffer(..))
6 | import Expect exposing (Expectation)
7 | import Fuzz exposing (Fuzzer, int, list, string)
8 | import Position exposing (Position)
9 | import Test exposing (..)
10 |
11 |
12 | suite : Test
13 | suite =
14 | describe "Buffer"
15 | [ describe "init"
16 | [ test "Buffer.init creates a buffer is properly initialized" <|
17 | \_ ->
18 | let
19 | buffer =
20 | Buffer.init "one\ntwo"
21 | in
22 | Expect.equal buffer (Buffer { content = "one\ntwo", lines = Array.fromList [ "one", "two" ] })
23 | , test "Buffer created by init is valid" <|
24 | \_ ->
25 | Buffer.init "one\ntwo"
26 | |> Buffer.valid
27 | |> Expect.equal True
28 | , test "Replace a string in the buffer" <|
29 | \_ ->
30 | Buffer.init "One\ntwo\nthree\nfour"
31 | |> Buffer.replace (Position 1 0) (Position 1 3) "TWO"
32 | |> Expect.equal (Buffer { content = "One\nTWO\nthree\nfour", lines = Array.fromList [ "One", "TWO", "three", "four" ] })
33 | ]
34 | , describe "Array.Util"
35 | [ test "insert (1)" <|
36 | \_ ->
37 | Array.fromList [ "aaa", "bbb" ]
38 | |> Array.Util.insert (Position 0 1) "X"
39 | |> Expect.equal (Array.fromList [ "aXaa", "bbb" ])
40 | , test "split" <|
41 | \_ ->
42 | Array.fromList [ "aaa", "xyz", "ccc" ]
43 | |> Array.Util.split (Position 1 1)
44 | |> Expect.equal (Array.fromList [ "aaa", "x", "yz", "xyz" ])
45 | , test "cut" <|
46 | \_ ->
47 | let
48 | result =
49 | { before = Array.fromList [ "abcde", "f" ]
50 | , middle = Array.fromList [ "ghij", "k" ]
51 | , after = Array.fromList [ "lmn", "opqr" ]
52 | }
53 | in
54 | Array.fromList [ "abcde", "fghij", "klmn", "opqr" ]
55 | |> Array.Util.cut (Position 1 1) (Position 2 1)
56 | |> Expect.equal result
57 | , test "cutOut" <|
58 | \_ ->
59 | Array.fromList [ "abcde", "fghij", "klmn", "opqr" ]
60 | |> Array.Util.cutOut (Position 1 1) (Position 2 1)
61 | |> Expect.equal ( Array.fromList [ "ghij", "k" ], Array.fromList [ "abcde", "f", "lmn", "opqr" ] )
62 | , test "replace" <|
63 | \_ ->
64 | Array.fromList [ "abcde", "fghij", "klmn", "opqr" ]
65 | |> Array.Util.replace (Position 1 1) (Position 2 1) "UVW"
66 | |> Expect.equal (Array.fromList [ "abcde", "f", "UVW", "lmn", "opqr" ])
67 | ]
68 | ]
69 |
--------------------------------------------------------------------------------
/src/Editor/Styles.elm:
--------------------------------------------------------------------------------
1 | module Editor.Styles exposing (editorStyles)
2 |
3 | import Html exposing (Html, text)
4 | import String.Interpolate exposing (interpolate)
5 |
6 |
7 | style : List (Html.Attribute msg) -> List (Html msg) -> Html msg
8 | style =
9 | Html.node "style"
10 |
11 |
12 | editorStyles : StyleConfig -> Html msg
13 | editorStyles styleConfig =
14 | style [] [ text (styleText styleConfig) ]
15 |
16 |
17 | type alias StyleConfig =
18 | { editorWidth : Float
19 | , editorHeight : Float
20 | , lineHeight : Float
21 | , fontProportion : Float
22 | }
23 |
24 |
25 | type alias StyleParams =
26 | { editorWidth : String
27 | , editorHeight : String
28 | , lineHeight : String
29 | , fontSize : String
30 | }
31 |
32 |
33 | getStyleParams : StyleConfig -> StyleParams
34 | getStyleParams c =
35 | { editorWidth = String.fromFloat c.editorWidth
36 | , editorHeight = String.fromFloat c.editorHeight
37 | , lineHeight = String.fromFloat c.lineHeight
38 | , fontSize = String.fromFloat (c.fontProportion * c.lineHeight)
39 | }
40 |
41 |
42 | styleText : StyleConfig -> String
43 | styleText styleConfig =
44 | let
45 | s =
46 | getStyleParams styleConfig
47 | in
48 | interpolate styleTemplate
49 | [ s.editorWidth
50 | , s.fontSize
51 | , s.lineHeight
52 | , s.editorHeight
53 | ]
54 |
55 |
56 | styleTemplate : String
57 | styleTemplate =
58 | """
59 |
60 | body { font-size: {1}px;
61 | line-height: {2}px;}
62 |
63 | .elm-editor-container {
64 | font-family: monospace;
65 | width: {0}px;
66 | user-select: none;
67 | -webkit-user-select: none;
68 | display: flex;
69 | // overflow-x : scroll;
70 | // overflow-y : scroll;
71 | // height: {3}px;
72 | }
73 |
74 | .elm-editor-container:focus {
75 | outline: none;
76 | // background-color : lightblue;
77 | }
78 |
79 | .elm-editor-gutter {
80 | display: flex;
81 | flex-direction: column;
82 | flex-shrink: 0;
83 | }
84 |
85 | .elm-editor-lines {
86 | flex-grow: 1;
87 | }
88 |
89 | .elm-editor-line-number {
90 | display: inline-block;
91 | width: 35px;
92 | padding-right: 5px;
93 | text-align: right;
94 | background-color: lightgray;
95 | cursor: default;
96 | }
97 |
98 | .elm-editor-line {
99 | cursor: text;
100 | }
101 |
102 | .elm-editor-line__gutter-padding {
103 | width: 5px;
104 | }
105 |
106 | .elm-editor-line__character--has-cursor {
107 | position: relative;
108 | }
109 |
110 | .elm-editor-line__character--selected {
111 | background-color: #8d9ffe;
112 | color: white;
113 | }
114 |
115 | .elm-editor-cursor {
116 | position: absolute;
117 | border-left: 16px solid #990000;
118 | opacity: 0.2;
119 | left: 0;
120 | height: 100%;
121 | }
122 |
123 | .elm-editor-container:focus .elm-editor-cursor {
124 | animation: 1s blink step-start infinite;
125 | border-left: 4px solid #333333;
126 | }
127 |
128 | @keyframes blink {
129 | from, to {
130 | opacity: 0;
131 | }
132 | 50% {
133 | opacity: 1;s
134 | }
135 | }
136 |
137 |
138 | body {
139 | font-family: sans-serif;
140 |
141 | }
142 |
143 | .center-column {
144 | display: flex;
145 | flex-direction: column;
146 | align-items: center;
147 | background-color: lightblue; //: #eeeeee;
148 | }
149 |
150 | #editor-container {
151 | text-align: left;
152 | }
153 |
154 |
155 |
156 | """
157 |
--------------------------------------------------------------------------------
/tests/BufferComparison.elm:
--------------------------------------------------------------------------------
1 | module BufferComparison exposing (suite)
2 |
3 | import Array
4 | import Array.Util
5 | import Buffer as Buffer1
6 | import Buffer2
7 | import Expect exposing (Expectation)
8 | import Fuzz exposing (Fuzzer, int, list, string)
9 | import Position exposing (Position)
10 | import Test exposing (..)
11 |
12 |
13 | suite : Test
14 | suite =
15 | describe "Compare Buffer implementations"
16 | [ describe "Basic operations"
17 | [ test "init" <|
18 | \_ ->
19 | let
20 | str =
21 | "one\ntwo"
22 |
23 | buffer1 =
24 | Buffer1.init str
25 | |> Buffer1.toString
26 | in
27 | str
28 | |> Buffer2.init
29 | |> Buffer2.toString
30 | |> Expect.equal buffer1
31 | , test "insert one character" <|
32 | \_ ->
33 | let
34 | str =
35 | "aaa\nbbb"
36 |
37 | buffer1 =
38 | str
39 | |> Buffer1.init
40 | |> Buffer1.insert (Position 0 1) "X"
41 | |> Buffer1.toString
42 |
43 | buffer2 =
44 | str
45 | |> Buffer2.init
46 | |> Buffer2.insert (Position 0 1) "X"
47 | |> Buffer2.toString
48 | in
49 | buffer1
50 | |> Expect.equal buffer2
51 | , test "insertion of one character with Buffer2 is valid " <|
52 | \_ ->
53 | let
54 | str =
55 | "aaa\nbbb"
56 |
57 | buffer2 =
58 | str
59 | |> Buffer2.init
60 | |> Buffer2.insert (Position 0 1) "X"
61 | in
62 | Buffer2.valid buffer2
63 | |> Expect.equal True
64 | , test "insert newline" <|
65 | \_ ->
66 | let
67 | str =
68 | "aaa\nbbb"
69 |
70 | buffer1 =
71 | str
72 | |> Buffer1.init
73 | |> Buffer1.insert (Position 0 1) "\n"
74 | |> Buffer1.toString
75 | |> Debug.log "1"
76 |
77 | buffer2 =
78 | str
79 | |> Buffer2.init
80 | |> Buffer2.insert (Position 0 1) "\n"
81 | |> Buffer2.toString
82 | |> Debug.log "2"
83 | in
84 | buffer1
85 | |> Expect.equal buffer2
86 | , test "insertion of newline with Buffer2 is valid " <|
87 | \_ ->
88 | let
89 | str =
90 | "aaa\nbbb"
91 |
92 | buffer2 =
93 | str
94 | |> Buffer2.init
95 | |> Buffer2.insert (Position 0 1) "\n"
96 | in
97 | Buffer2.valid buffer2
98 | |> Expect.equal True
99 | ]
100 | ]
101 |
--------------------------------------------------------------------------------
/demo/public/assets/custom-element-config.js:
--------------------------------------------------------------------------------
1 | MathJax = {
2 | tex: {inlineMath: [['$', '$'], ['\\(', '\\)']]},
3 | options: {
4 | skipHtmlTags: {'[+]': ['math-text']}
5 | },
6 | startup: {
7 | ready: () => {
8 | //
9 | // Get some MathJax objects from the MathJax global
10 | //
11 | // (Ideally, you would turn this into a custom component, and
12 | // then these could be handled by normal imports, but this is
13 | // just an example and so we use an expedient method of
14 | // accessing these for now.)
15 | //
16 | const mathjax = MathJax._.mathjax.mathjax;
17 | const HTMLAdaptor = MathJax._.adaptors.HTMLAdaptor.HTMLAdaptor;
18 | const HTMLHandler = MathJax._.handlers.html.HTMLHandler.HTMLHandler;
19 | const AbstractHandler = MathJax._.core.Handler.AbstractHandler.prototype;
20 | const startup = MathJax.startup;
21 |
22 | //
23 | // Extend HTMLAdaptor to handle shadowDOM as the document
24 | //
25 | class ShadowAdaptor extends HTMLAdaptor {
26 | create(kind, ns) {
27 | const document = (this.document.createElement ? this.document : this.window.document);
28 | return (ns ?
29 | document.createElementNS(ns, kind) :
30 | document.createElement(kind));
31 | }
32 | text(text) {
33 | const document = (this.document.createTextNode ? this.document : this.window.document);
34 | return document.createTextNode(text);
35 | }
36 | head(doc) {
37 | return doc.head || (doc.firstChild || {}).firstChild || doc;
38 | }
39 | body(doc) {
40 | return doc.body || (doc.firstChild || {}).lastChild || doc;
41 | }
42 | root(doc) {
43 | return doc.documentElement || doc.firstChild || doc;
44 | }
45 | }
46 |
47 | //
48 | // Extend HTMLHandler to handle shadowDOM as document
49 | //
50 | class ShadowHandler extends HTMLHandler {
51 | create(document, options) {
52 | const adaptor = this.adaptor;
53 | if (typeof(document) === 'string') {
54 | document = adaptor.parse(document, 'text/html');
55 | } else if ((document instanceof adaptor.window.HTMLElement ||
56 | document instanceof adaptor.window.DocumentFragment) &&
57 | !(document instanceof window.ShadowRoot)) {
58 | let child = document;
59 | document = adaptor.parse('', 'text/html');
60 | adaptor.append(adaptor.body(document), child);
61 | }
62 | //
63 | // We can't use super.create() here, since that doesn't
64 | // handle shadowDOM correctly, so call HTMLHandler's parent class
65 | // directly instead.
66 | //
67 | return AbstractHandler.create.call(this, document, options);
68 | }
69 | }
70 |
71 | //
72 | // Register the new handler and adaptor
73 | //
74 | startup.registerConstructor('HTMLHandler', ShadowHandler);
75 | startup.registerConstructor('browserAdaptor', () => new ShadowAdaptor(window));
76 |
77 | //
78 | // A service function that creates a new MathDocument from the
79 | // shadow root with the configured input and output jax, and then
80 | // renders the document. The MathDocument is returned in case
81 | // you need to rerender the shadowRoot later.
82 | //
83 | MathJax.typesetShadow = function (root) {
84 | const InputJax = startup.getInputJax();
85 | const OutputJax = startup.getOutputJax();
86 | const html = mathjax.document(root, {InputJax, OutputJax});
87 | html.render();
88 | return html;
89 | }
90 |
91 | //
92 | // Now do the usual startup now that the extensions are in place
93 | //
94 | MathJax.startup.defaultReady();
95 | }
96 | }
97 | };
98 |
--------------------------------------------------------------------------------
/demo/public/assets/style.css:
--------------------------------------------------------------------------------
1 | .flex-column {
2 | display: flex;
3 | flex-direction: column;
4 | }
5 |
6 | .flex-column-space {
7 | display: flex;
8 | flex-direction: column;
9 | justify-content: space-between;
10 | }
11 |
12 |
13 | .flex-row {
14 | display: flex;
15 | flex-direction: row;
16 | }
17 |
18 | .flex-row-text-aligned {
19 | display: flex;
20 | flex-direction: row;
21 | align-items: baseline;
22 | }
23 |
24 |
25 |
26 |
27 | .elmsh-line {
28 | padding : 8px;
29 | line-height: 12px !important;
30 | }
31 |
32 |
33 | .mm-paragraph {
34 | white-space: normal;
35 | }
36 |
37 | .toc-level-0 { margin-left: 0em; font-size : 10pt; line-height: 4pt; font-weight: normal; text-decoration: none; }
38 | .toc-level-1 { margin-left: 1em; font-size : 10pt; line-height: 4pt; font-weight: normal; text-decoration: none; }
39 | .toc-level-2 { margin-left: 2em; font-size : 10pt; line-height: 4pt; font-weight: normal; text-decoration: none; }
40 | .toc-level-3 { margin-left: 3em; font-size : 10pt; line-height: 4pt; font-weight: normal; text-decoration: none; }
41 |
42 | .mm-code {font-family: "Courier New"
43 | , Courier, monospace; padding: 8px; background-color: #f5f5f5; font-size: 11pt;
44 | }
45 | .mm-inline-code {font-family: "Courier New"
46 | , Courier, monospace; padding-left: 5px; padding-right: 5px;background-color: #f5f5f5; font-size: 11pt;
47 | }
48 |
49 | .mm-image {width : 100%; }
50 | .mm-image-left {float: left; width: 40%; margin-right: 12px;}
51 | .mm-image-right {float: right; width: 40%; margin-left: 4px;}
52 | .mm-image-center {
53 | display: block;
54 | margin-left: auto;
55 | margin-right: auto;
56 | width: 60%;
57 | }
58 |
59 | .mm-h1 {font-size: 24px;}
60 | .mm-h2 {font-size: 20px;}
61 | .mm-h3 {font-size: 16px}
62 | .mm-h4 {font-size: 12px;}
63 | .mm-h5 {font-size: 12px;}
64 |
65 |
66 |
67 | .highlight {
68 | background-color: #8d9ffe;
69 | }
70 |
71 | .mm-block-0 {
72 | }
73 |
74 | .mm-block-1 {
75 | margin-left: 16px;
76 | }
77 |
78 | .mm-block-2 {
79 | margin-left: 32px;
80 | }
81 |
82 |
83 | .mm-block-3 {
84 | margin-left: 48px;
85 | }
86 |
87 | .mm-block-4 {
88 | margin-left: 64px;
89 | }
90 |
91 |
92 | .mm-olist-item {
93 | list-style: none;
94 | margin-bottom: 8px;
95 | padding-left: 18px;
96 | text-indent: -18px;
97 | }
98 |
99 | .mm-olist-item.mm-block-0 {
100 | margin-left: 12px;
101 | }
102 |
103 | .mm-olist-item.mm-block-1 {
104 | margin-left: 12px;
105 | }
106 |
107 |
108 | .mm-ulist-item {
109 | list-style: none;
110 | margin-bottom: 8px;
111 | padding-left: 18px;
112 | text-indent: -12px;
113 | white-space: normal;
114 | }
115 |
116 | .mm-ulist-item.mm-block-0 {
117 | margin-left: 8px;
118 | }
119 |
120 | .mm-quotation {
121 | margin-top: 18px;
122 | margin-right: 36px;
123 | margin-bottom: 18px;
124 | white-space: normal;
125 | }
126 | .mm-poetry {
127 | margin-left: 24px;
128 | margin-top: 18px;
129 | margin-right: 36px;
130 | margin-bottom: 18px;
131 | white-space: nowrap;
132 | }
133 | .mm-closed-block {
134 | margin-bottom; 12px;
135 | width: inherit;
136 | white-space: normal;
137 | }
138 | .mm-closed-block .mm-inlinelist {
139 | margin-bottom: 12px;
140 | line-height: 18px;
141 | width: inherit;
142 | }
143 | .mm-poetry .mm-inlinelist .mm-inlinelist {
144 | margin-bottom: 4px;
145 | width: inherit;
146 | }
147 | .mm-error-message {
148 | color: red; margin-bottom: 12px;
149 | }
150 | .mm-code {
151 | font: 16px courier; background-color: #eee; padding: 8px;
152 | }
153 |
154 | .mm-strike-through {
155 | text-decoration: line-through;
156 | }
157 |
158 |
159 | .mm-table-cell {
160 | padding: 4px;
161 | margin-right : 3px;
162 | margin-bottom: 6px;
163 | background-color: #eef;
164 | }
165 |
166 |
167 | .mm-paragraph.mm-block-2 {
168 | margin: 0;
169 | }
170 |
171 |
172 | .mm-table-row {
173 | display: block;
174 | margin-left: 40px;
175 | height: 25px;
176 | }
177 |
178 | p.mm-table-row {
179 | display: inline-block;
180 | }
181 |
182 | .mm-table-cell {
183 | display: inline-block;
184 | width: 80px;
185 | }
186 |
187 | p.mm-table-cell {
188 | display: inline-block;
189 | }
190 |
191 |
192 | .mm-verbatim {
193 | font: 16px courier; background-color: #eee; padding: 8px;
194 | }
195 |
--------------------------------------------------------------------------------
/docs/Embedding.md:
--------------------------------------------------------------------------------
1 | # Editor Installation
2 |
3 | To install the Editor in app, use the outline below,
4 | consulting the code in `./demo`
5 |
6 | ## Installations
7 |
8 |
9 | ```bash
10 | elm install lukewestby/elm-string-interpolate
11 | elm install carwow/elm-slider
12 | elm install lovasoa/elm-rolling-list
13 | elm install elm-community/string-extra
14 | elm install elm-community/maybe-extra
15 | elm install elm-community/list-extra
16 | elm install folkertdev/elm-paragraph
17 | ```
18 |
19 | ## Imports
20 |
21 | ```
22 | import Editor exposing (EditorConfig, Editor, EditorMsg)
23 | import Editor.Config exposing (WrapOption(..))
24 | import Editor.Strings
25 | import Editor.Update -- for external copy-paste if needed
26 | import SingleSlider as Slider
27 |
28 | ```
29 |
30 | ## Msg
31 |
32 | ```elm
33 | type Msg
34 | = EditorMsg EditorMsg
35 | | SliderMsg Slider.Msg
36 | | Outside Outside.InfoForElm -- if using module Outside
37 | | LogErr String -- if using module Outside
38 |
39 | | ...
40 | ```
41 |
42 | ## Model
43 |
44 | ```elm
45 | type alias Model =
46 | { editor : Editor
47 | , clipboard : String
48 | , ...
49 | }
50 | ```
51 |
52 | ## Init
53 |
54 | ```elm
55 | init : () -> ( Model, Cmd Msg )
56 | init () =
57 | ( { editor = Editor.init config "Some text ..."
58 | , clipboard = ""
59 | }
60 | , Cmd.none
61 | )
62 | ```
63 |
64 |
65 | where (for example):
66 |
67 | ```elm
68 | config : EditorConfig Msg
69 | config =
70 | { editorMsg = EditorMsg
71 | , sliderMsg = SliderMsg
72 | , editorStyle = editorStyle
73 | , width = 500
74 | , lines = 30
75 | , lineHeight = 16.0
76 | , showInfoPanel = True
77 | , wrapParams = { maximumWidth = 55, optimalWidth = 50, stringWidth = String.length }
78 | , wrapOption = DontWrap
79 | }
80 | ```
81 |
82 | ```
83 | editorStyle : List (Html.Attribute msg)
84 | editorStyle =
85 | [ HA.style "background-color" "#dddddd"
86 | , HA.style "border" "solid 0.5px"
87 | ]
88 | ```
89 |
90 |
91 | ## Update
92 |
93 | ```elm
94 | update : Msg -> Model -> ( Model, Cmd Msg )
95 | update msg model =
96 | case msg of
97 | EditorMsg editorMsg ->
98 | let
99 | -- needed for ezxternal copy-paste:
100 | clipBoardCmd =
101 | if editorMsg == Editor.Update.CopyPasteClipboard then
102 | Outside.sendInfo (Outside.AskForClipBoard E.null)
103 |
104 | else
105 | Cmd.none
106 |
107 | ( editor, cmd ) =
108 | Editor.update editorMsg model.editor
109 | in
110 | ( { model | editor = editor }
111 | , Cmd.batch [ clipBoardCmd, Cmd.map EditorMsg cmd ] )
112 |
113 | SliderMsg sliderMsg ->
114 | let
115 | ( newEditor, cmd ) =
116 | Editor.sliderUpdate sliderMsg model.editor
117 | in
118 | ( { model | editor = newEditor }, cmd |> Cmd.map SliderMsg )
119 |
120 | -- The below are optional, and used for external copy/pastes
121 | -- See module `Outside` and also `outside.js` and `index.html` for additional
122 | -- information
123 |
124 | Outside infoForElm ->
125 | case infoForElm of
126 |
127 | Outside.GotClipboard clipboard ->
128 | ({model | clipboard = clipboard}, Cmd.none)
129 |
130 |
131 |
132 | Other cases ...
133 | ```
134 |
135 | ## Subscriptions
136 |
137 | ```elm
138 | subscriptions : Model -> Sub Msg
139 | subscriptions model =
140 | Sub.batch
141 | [ Sub.map SliderMsg <|
142 | Slider.subscriptions (Editor.slider model.editor)
143 | ]
144 | ```
145 |
146 | ## View
147 |
148 | ```elm
149 | view : Model -> Html Msg
150 | view model =
151 | div [ HA.style "margin" "60px" ]
152 | [ title -- for example
153 | , Editor.embedded config model.editor
154 | , footer model -- for example
155 | ]
156 | ```
157 |
158 | ## Files
159 |
160 | - Use the `index.html` file in `./demo` as a starting point for your `index.html`
161 | - Copy the files `outside.js` and `Outside.elm` if you are
162 | implementing external copy-paste
--------------------------------------------------------------------------------
/src/Buffer2.elm:
--------------------------------------------------------------------------------
1 | module Buffer2 exposing
2 | ( Buffer(..)
3 | , init
4 | , insert
5 | , nearWordChar
6 | , replace
7 | , toString
8 | , valid
9 | )
10 |
11 | import Array exposing (Array)
12 | import Array.Util
13 | import BufferHelper as BH
14 | import List.Extra
15 | import Maybe.Extra
16 | import Position exposing (Position)
17 | import String.Extra
18 |
19 |
20 | {-| Returns True iff the buffer is valid
21 |
22 | valid (init "One\ntwo\nthree\nfour")
23 | --> True
24 |
25 | bb2 : Buffer
26 | bb2 = Buffer { content = "One\ntwo\nthree\nfour", lines = Array.fromList ["One","two!","three","four"] }
27 |
28 | valid bb2
29 | --> False
30 |
31 | -}
32 | type Buffer
33 | = Buffer { content : String, lines : Array String }
34 |
35 |
36 | {-| Create a new buffer from a string
37 |
38 | import Array
39 |
40 | init "one\ntwo"
41 | --> Buffer { content = "one\ntwo", lines = Array.fromList ["one","two"] }
42 |
43 | -}
44 | init : String -> Buffer
45 | init content =
46 | Buffer { content = content, lines = Array.fromList (String.lines content) }
47 |
48 |
49 | valid : Buffer -> Bool
50 | valid (Buffer data) =
51 | (data.lines |> Array.toList |> String.join "\n") == data.content
52 |
53 |
54 | toString : Buffer -> String
55 | toString (Buffer data) =
56 | data.content
57 |
58 |
59 | {-| Returns true if the Position is at or after a word character. See isWordChar.
60 |
61 | import Position exposing(..)
62 |
63 | nearWordChar (Position 0 0) (init "one\ntwo")
64 | --> True
65 |
66 | nearWordChar (Position 0 10) (init "one\ntwo")
67 | --> False
68 |
69 | -}
70 | nearWordChar : Position -> Buffer -> Bool
71 | nearWordChar position (Buffer data) =
72 | BH.indexFromPosition data.content position
73 | |> Maybe.andThen
74 | (\index ->
75 | let
76 | previousChar =
77 | BH.stringCharAt (index - 1) data.content
78 |
79 | currentChar =
80 | BH.stringCharAt index data.content
81 | in
82 | Maybe.map BH.isWordChar previousChar
83 | |> Maybe.Extra.orElseLazy
84 | (\() -> Maybe.map BH.isWordChar currentChar)
85 | )
86 | |> Maybe.withDefault False
87 |
88 |
89 | {-| Insert a string into the buffer.
90 |
91 | import Array
92 |
93 | import Position
94 |
95 | bb: Buffer
96 | bb = init "aaa\nbbb"
97 | --> Buffer { content = "aaa\nbbb", lines = Array.fromList ["aaa","bbb"] }
98 |
99 | insert (Position 0 1) "X" bb
100 | --> Buffer { content = "aXaa\nbbb", lines = Array.fromList ["aXaa","bbb"] }
101 |
102 | valid bb2
103 | --> True
104 |
105 | -}
106 | insert : Position -> String -> Buffer -> Buffer
107 | insert position str (Buffer data) =
108 | let
109 | content =
110 | BH.indexFromPosition data.content position
111 | |> Maybe.map (\index -> String.Extra.insertAt str index data.content)
112 | |> Maybe.withDefault data.content
113 |
114 | lines =
115 | Array.Util.insert position str data.lines
116 | in
117 | Buffer { content = content, lines = lines }
118 |
119 |
120 | {-| Replace the string between two positions with a different string.
121 |
122 | import Array
123 |
124 | import Position
125 |
126 |
127 | bb : Buffer
128 | bb =
129 | init "One\ntwo\nthree\nfour"
130 |
131 |
132 | replace (Position 1 0) (Position 1 3) "TWO" bb
133 | --> Buffer { content = "One\nTWO\nthree\nfour", lines = Array.fromList ["One","TWO","three","four"] }
134 |
135 | replace (Position 1 0) (Position 2 3) "TWO\nTHREE" bb
136 |
137 | -}
138 | replace : Position -> Position -> String -> Buffer -> Buffer
139 | replace pos1 pos2 str (Buffer data) =
140 | let
141 | ( start, end ) =
142 | Position.order pos1 pos2
143 |
144 | content : String
145 | content =
146 | Maybe.map2
147 | (\startIndex endIndex ->
148 | String.slice 0 startIndex data.content
149 | ++ str
150 | ++ String.dropLeft endIndex data.content
151 | )
152 | (BH.indexFromPosition data.content start)
153 | (BH.indexFromPosition data.content end)
154 | |> Maybe.withDefault data.content
155 | in
156 | Buffer { content = content, lines = Array.Util.replace start end str data.lines }
157 |
158 |
159 |
160 | -- HELPER FUNCTIONS THAT DO NOT REFERENCE BUFFER --
161 |
--------------------------------------------------------------------------------
/testSpares/UnitTest.elm:
--------------------------------------------------------------------------------
1 | module UnitTest exposing (suite)
2 |
3 | import Buffer exposing (..)
4 | import Debounce
5 | import Editor exposing (EditorConfig, EditorMsg)
6 | import Editor.Config exposing (WrapOption(..))
7 | import Editor.Function as F exposing (bufferOf, stateOf)
8 | import Editor.History
9 | import Editor.Model exposing (InternalState)
10 | import Editor.Update exposing (Msg(..))
11 | import Expect exposing (Expectation)
12 | import Fuzz exposing (Fuzzer, int, list, string)
13 | import Position exposing (Position)
14 | import RollingList
15 | import Test exposing (..)
16 |
17 |
18 | testBuffer =
19 | Buffer "a\nbc"
20 |
21 |
22 | type Msg
23 | = EditorMsg EditorMsg
24 |
25 |
26 | editorConfig : EditorConfig Msg
27 | editorConfig =
28 | { editorMsg = EditorMsg
29 | , width = 450
30 | , height = 544
31 | , lineHeight = 16.0
32 | , showInfoPanel = False
33 | , wrapParams = { maximumWidth = 55, optimalWidth = 50, stringWidth = String.length }
34 | , wrapOption = DontWrap
35 | }
36 |
37 |
38 | state : InternalState
39 | state =
40 | { config = Editor.transformConfig editorConfig
41 | , cursor = Position 0 0
42 | , selection = Nothing
43 | , selectedText = Nothing
44 | , clipboard = ""
45 | , currentLine = Nothing
46 | , dragging = False
47 | , history = Editor.History.empty
48 | , searchTerm = ""
49 | , replacementText = ""
50 | , canReplace = False
51 | , searchResults = RollingList.fromList []
52 | , showHelp = True
53 | , showInfoPanel = editorConfig.showInfoPanel
54 | , showGoToLinePanel = False
55 | , showSearchPanel = False
56 | , savedBuffer = Buffer.fromString "abc\ndef"
57 | , debounce = Debounce.init
58 | , topLine = 0
59 | , searchHitIndex = 0
60 | }
61 |
62 |
63 | suite : Test
64 | suite =
65 | describe "Editor update"
66 | [ describe "CursorForward"
67 | -- Nest as many descriptions as you like.
68 | [ test "has no effect on a palindrome" <|
69 | \_ ->
70 | let
71 | palindrome =
72 | "hannah"
73 | in
74 | Expect.equal palindrome (String.reverse palindrome)
75 |
76 | -- Expect.equal is designed to be used in pipeline style, like this.
77 | , test "reverses a known string" <|
78 | \_ ->
79 | "ABCDEFG"
80 | |> String.reverse
81 | |> Expect.equal "GFEDCBA"
82 |
83 | -- fuzz runs the test 100 times with randomly-generated inputs!
84 | , fuzz string "restores the original string if you run it again" <|
85 | \randomlyGeneratedString ->
86 | randomlyGeneratedString
87 | |> String.reverse
88 | |> String.reverse
89 | |> Expect.equal randomlyGeneratedString
90 | , test "Cursor forward, inside line" <|
91 | \_ ->
92 | let
93 | buffer =
94 | Buffer.fromString "abc\ndef"
95 |
96 | state1 =
97 | { state | cursor = Position 0 1 }
98 |
99 | state2 =
100 | F.cursorRight state1 buffer |> stateOf
101 | in
102 | Expect.equal state2.cursor (Position 0 2)
103 | , test "Cursor forward, inside line, correct character" <|
104 | \_ ->
105 | let
106 | buffer =
107 | Buffer.fromString "abc\ndef"
108 |
109 | state1 =
110 | { state | cursor = Position 0 1 }
111 |
112 | state2 =
113 | F.cursorRight state1 buffer |> stateOf
114 | in
115 | Expect.equal (String.slice 2 3 (Buffer.toString buffer)) "c"
116 | , test "Cursor forward just before end of line" <|
117 | \_ ->
118 | let
119 | buffer =
120 | Buffer.fromString "abc\ndef"
121 |
122 | state1 =
123 | { state | cursor = Position 0 2 }
124 |
125 | state2 =
126 | F.cursorRight state1 buffer |> stateOf
127 | in
128 | Expect.equal state2.cursor (Position 0 3)
129 | ]
130 | ]
131 |
--------------------------------------------------------------------------------
/src/Array/Util.elm:
--------------------------------------------------------------------------------
1 | module Array.Util exposing (Position, cut, cutOut, insert, put, replace, split, splitStringAt)
2 |
3 | import Array exposing (Array)
4 | import List.Extra
5 | import String.Extra
6 |
7 |
8 | type alias Position =
9 | { line : Int, column : Int }
10 |
11 |
12 | type alias StringZipper =
13 | { before : Array String
14 | , middle : Array String
15 | , after : Array String
16 | }
17 |
18 |
19 | {-|
20 |
21 | arrL = Array.fromList ["aaa", "bbb"]
22 |
23 | insert (Position 0 1) "X" arrL
24 | --> Array.fromList ["aXaa","bbb"]
25 |
26 | insert (Position 1 1) "X" arrL
27 | --> Array.fromList ["aaa","bXbb"]
28 |
29 | -}
30 | insert : Position -> String -> Array String -> Array String
31 | insert position str array =
32 | case Array.get position.line array of
33 | Nothing ->
34 | array
35 |
36 | Just line ->
37 | let
38 | newLine =
39 | String.Extra.insertAt str position.column line
40 | in
41 | Array.set position.line newLine array
42 |
43 |
44 | {-|
45 |
46 | arr : Array String
47 | arr = Array.fromList ["aaa","xyz","ccc"]
48 |
49 | split (Position 1 1) arr
50 | Array.fromList ["aaa","x","yz","xyz"]
51 |
52 | -}
53 | split : Position -> Array String -> Array String
54 | split position array =
55 | case Array.get position.line array of
56 | Nothing ->
57 | array
58 |
59 | Just focus ->
60 | let
61 | focusLength =
62 | String.length focus
63 |
64 | before =
65 | Array.slice 0 position.line array
66 |
67 | after =
68 | Array.slice position.line (focusLength - 1) array
69 |
70 | beforeSuffix =
71 | String.slice 0 position.column focus
72 |
73 | afterPrefix =
74 | String.slice position.column focusLength focus
75 |
76 | firstPart =
77 | Array.push beforeSuffix before
78 |
79 | secondPart =
80 | put afterPrefix after
81 | in
82 | Array.append firstPart secondPart
83 |
84 |
85 | splitStringAt : Int -> String -> ( String, String )
86 | splitStringAt k str =
87 | let
88 | n =
89 | String.length str
90 | in
91 | ( String.slice 0 k str, String.slice k n str )
92 |
93 |
94 | {-|
95 |
96 | arr =
97 | Array.fromList [ "abcde", "fghij", "klmn", "opqr" ]
98 |
99 | cut (Position 1 1) (Position 2 1) arr
100 | --> { before = Array.fromList ["abcde","f"]
101 | , middle = Array.fromList ["ghij","k"]
102 | , after = Array.fromList ["lmn","opqr"]
103 | }
104 |
105 | -}
106 | cut : Position -> Position -> Array String -> StringZipper
107 | cut pos1 pos2 array =
108 | let
109 | n =
110 | Array.length array
111 |
112 | before_ =
113 | Array.slice 0 pos1.line array
114 |
115 | ( a, b ) =
116 | Array.get pos1.line array |> Maybe.withDefault "" |> splitStringAt pos1.column
117 |
118 | middle_ =
119 | Array.slice (pos1.line + 1) pos2.line array
120 |
121 | m =
122 | Array.length middle_
123 |
124 | ( c, d ) =
125 | Array.get pos2.line array |> Maybe.withDefault "" |> splitStringAt pos2.column
126 |
127 | after_ =
128 | Array.slice (pos2.line + 1) n array
129 | in
130 | { before = Array.push a before_
131 | , middle = Array.push c (put b middle_)
132 | , after = put d after_
133 | }
134 |
135 |
136 | {-|
137 |
138 | arr =
139 | Array.fromList [ "abcde", "fghij", "klmn", "opqr" ]
140 |
141 | cutOut (Position 1 1) (Position 2 1) arr
142 | --> (Array.fromList ["ghij","k"], Array.fromList ["abcde","f","lmn","opqr"])
143 |
144 | -}
145 | cutOut : Position -> Position -> Array String -> ( Array String, Array String )
146 | cutOut pos1 pos2 array =
147 | cut pos1 pos2 array
148 | |> (\sz -> ( sz.middle, Array.append sz.before sz.after ))
149 |
150 |
151 | {-| Assume pos1 < pos2
152 |
153 | replace (Position 1 1) (Position 2 1) "UVW" arr
154 | --> Array.fromList ["abcde","f","UVW","lmn","opqr"]
155 |
156 | -}
157 | replace : Position -> Position -> String -> Array String -> Array String
158 | replace pos1 pos2 str array =
159 | case pos1.line == pos2.line of
160 | True ->
161 | case Array.get pos1.line array of
162 | Nothing ->
163 | array
164 |
165 | Just line ->
166 | let
167 | newLine =
168 | String.Extra.replaceSlice str pos1.column pos2.column line
169 | in
170 | Array.set pos1.line newLine array
171 |
172 | False ->
173 | let
174 | sz =
175 | cut pos1 pos2 array
176 | in
177 | Array.append (Array.push str sz.before) sz.after
178 |
179 |
180 | put : String -> Array String -> Array String
181 | put str array =
182 | Array.append (Array.fromList [ str ]) array
183 |
--------------------------------------------------------------------------------
/src/Editor/Strings.elm:
--------------------------------------------------------------------------------
1 | module Editor.Strings exposing (help, info)
2 |
3 | {-| Strings for the help system.
4 |
5 | @docs help, info
6 |
7 | -}
8 |
9 |
10 | {-| Help text for the editor
11 | -}
12 | help : String
13 | help =
14 | """
15 | ------------------------------------------
16 | Key commands
17 | ------------------------------------------
18 |
19 | NEW: ctrl-shift-S to sync with external
20 | window if the hosting app implements it.
21 | Still flaky. Aslo: ctrl-shift-C to copy
22 | text to the system keyboard, ctrl-shift-V
23 | to paste text from the system keyboard
24 | to the editor. For now, Chrome only.
25 |
26 | Show help ctrl-h (Toggle)
27 | Show info panel ctrl-shift-i (Toggle)
28 |
29 | Cursor
30 | ------
31 |
32 | Forward right-arrow
33 | Backwards left-arrow
34 | Start of line option-left-arrow or Home
35 | End of line option-right-arrow or End
36 |
37 | Line Up up-arrow
38 | Line Down down-arrow
39 |
40 | Up many lines option up-arrow
41 | Down many lines option down-arrow
42 |
43 | First line ctrl-option up-arrow
44 | Last line ctrl-option down-arrow
45 |
46 | Go to line ctrl-g (Toggle)
47 |
48 |
49 | Selection
50 | ---------
51 | Select word Double-click
52 | Select line Triple-click
53 | Select group ctrl-d
54 | Select all ctrl-shift-a
55 |
56 | Extend selection shift-arrow
57 | (up | down | left | right)
58 |
59 | Copy selection ctrl-c
60 | Cut selection ctrl-x
61 | Paste selection ctrl-v
62 |
63 | External copy-paste
64 | -------------------
65 |
66 | - ctrl-shift-C copies selected text to the
67 | system clipboard.
68 |
69 | - ctrl-shift-V copies text from the system
70 | clipboard and pastes the content to the
71 | editor at current cursor. The copied
72 | text remains in the Editor clipboard.
73 |
74 | The pasted text will be wrapped if the
75 | the WrapOption is on.
76 |
77 | This works in Chrome 79 but not Firefox.
78 | In Chrome you have to respond to a permission
79 | dialog each time. I'll see if this can
80 | be reduced to once per session.
81 |
82 | Text
83 | ------------
84 |
85 | Indent Tab
86 | De-indent shift-Tab
87 |
88 | Wrap selection ctrl-w
89 | Wrap all ctrl-shift-w
90 | Toggle wrapping ctrl-option-w
91 |
92 | Typing ctrl-shift-w at the end
93 | of a paragraph will wrap it.
94 |
95 | Clear all ctrl-option c
96 |
97 | Search
98 | ------
99 |
100 | Search panel ctrl-s (Toggle)
101 | Replace panel ctrl-r (Toggle)
102 | Next search hit ctrl-. (Think >)
103 | Prev search hit ctrl-. (Think <)
104 |
105 | Undo/Redo
106 | ----------
107 |
108 | Undo ctrl-z
109 | Redo ctrl-y
110 |
111 | """
112 |
113 |
114 | {-| Information about the Pure Elm Editor project.
115 | -}
116 | info : String
117 | info =
118 | """This is a first test of how
119 | the editor could be used as a package.
120 | The 'Info' button is a proxy for loading new
121 | content into the editor from an external
122 | source.
123 |
124 | Everything in this window is from `Editor.view`. All
125 | the rest is in `Main`, though of course it uses
126 | functions exported by `Editor`, e.g., the slider.
127 |
128 | The "Reset" button loads the initial text.
129 | The "Gettysburg address" button loads
130 | Abraham Lincoln's speech. It contains three
131 | very long lines which are wrapped by the editor
132 | before you see them. I need to devote more
133 | thought to how best to do text wrapping —
134 | it needs to be an option, for instance.
135 | Am using Folkert de Vries' *elm-paragraph*
136 | library for this.
137 |
138 | NOTES
139 |
140 | 1. At present, all the editor controls
141 | are key commands. Press the "Help" button,
142 | in hte info panel, upper right, for a partial
143 | list of these. Full list coming soon.
144 |
145 | 2. More parameters are now configurable.
146 | For example, one can do this in Main:
147 |
148 | editorState = Editor.init
149 | { defaultConfig | lines = 30
150 | , showInfoPanel = False
151 | }
152 |
153 | to set up the embedded editor with a 30-line
154 | display and the info panel not shown.
155 |
156 | 3. I've tested the editor on a file of 1700
157 | lines and 7700 words. It works fine. For
158 | a file of 17,000 lines and 770,000 words,
159 | moving around the text is extremely fast,
160 | whereas double-clicking to select a word
161 | fails. I don't know why as of this writing.
162 |
163 | 4. In view of (3), I've set the gutter width
164 | to accommodate files of up to 9,999 lines.
165 |
166 | ROADMAP
167 |
168 | There is still a lot to do.
169 |
170 |
171 |
172 | """
173 |
174 |
175 | external =
176 | """
177 | External copy-paste
178 | -------------------
179 |
180 | - ctrl-shift-u copies text from the system
181 | clipboard and pastes the content to the
182 | editor at current cursor. The copied
183 | text remains in the Editor clipboard.
184 |
185 | - ctrl-shift-v pastes text from Editor
186 | clipboard
187 |
188 | The pasted text will be wrapped if the
189 | the WrapOption is on.
190 |
191 | This works in Chrome 79 but not Firefox.
192 | In Chrome you have to respond to a permission
193 | dialog each time. I'll see if this can
194 | be reduced to once per session.
195 | """
196 |
--------------------------------------------------------------------------------
/testSpares/Tests/Common.elm:
--------------------------------------------------------------------------------
1 | module Tests.Common exposing (Msg(..), app, config, cursorDown, cursorLeft, cursorRight, cursorUp, firstLine, initModel, insert, insertBlank, insertOne, lastLine, modelTemplate, modelToString, msgTostring, noOp, oneCharString, removeCharAfter, removeCharBefore, renderVisible, weightedMsgFuzzer)
2 |
3 | import ArchitectureTest exposing (..)
4 | import Editor exposing (..)
5 | import Editor.Config exposing (WrapOption(..))
6 | import Editor.Update exposing (..)
7 | import Fuzz exposing (Fuzzer, int, list, string)
8 | import String.Interpolate exposing (interpolate)
9 |
10 |
11 | app : TestedApp Editor EditorMsg
12 | app =
13 | { model = ConstantModel initModel
14 | , update = NormalUpdate Editor.update
15 | , msgFuzzer = Fuzz.constant CursorRight -- weightedMsgFuzzer
16 | , msgToString = msgTostring
17 | , modelToString = modelToString
18 | }
19 |
20 |
21 | type Msg
22 | = EditorMsg EditorMsg
23 |
24 |
25 | modelToString : Editor -> String
26 | modelToString editor =
27 | let
28 | pos =
29 | getCursor editor
30 |
31 | line =
32 | pos.line |> String.fromInt
33 |
34 | column =
35 | pos.column |> String.fromInt
36 |
37 | source =
38 | getSource editor
39 |
40 | decoratedSource =
41 | source
42 | |> String.replace " " "°"
43 | |> String.lines
44 | |> List.map (\line_ -> (line_ |> String.length |> String.fromInt) ++ ": " ++ line_)
45 | |> List.indexedMap (\i line_ -> String.fromInt i ++ ", " ++ line_)
46 | |> String.join "\n"
47 | in
48 | interpolate modelTemplate [ line, column, decoratedSource ]
49 |
50 |
51 | renderVisible : String -> String
52 | renderVisible str =
53 | str
54 | |> String.replace " " "°"
55 | |> String.replace "\n" "(NL)"
56 |
57 |
58 | modelTemplate =
59 | "MODEL\ncursor = ({0}, {1})\nbuffer:\n{2}\n"
60 |
61 |
62 | msgTostring : EditorMsg -> String
63 | msgTostring editorMsg =
64 | case editorMsg of
65 | NoOp ->
66 | "NoOp"
67 |
68 | CursorUp ->
69 | "CursorUp"
70 |
71 | CursorDown ->
72 | "CursorDown"
73 |
74 | CursorLeft ->
75 | "CursorLeft"
76 |
77 | CursorRight ->
78 | "CursorRight"
79 |
80 | Insert str ->
81 | "Insert: " ++ renderVisible str
82 |
83 | RemoveCharBefore ->
84 | "RemoveCharBefore"
85 |
86 | RemoveCharAfter ->
87 | "RemoveCharAfter"
88 |
89 | FirstLine ->
90 | "CursorRight"
91 |
92 | LastLine ->
93 | "CursorRight"
94 |
95 | _ ->
96 | "Undefined"
97 |
98 |
99 | initModel : Editor
100 | initModel =
101 | Editor.init config "a"
102 |
103 |
104 |
105 | -- "abc\ndefg\n"
106 |
107 |
108 | config : EditorConfig Msg
109 | config =
110 | { editorMsg = EditorMsg
111 | , width = 450
112 | , height = 544
113 | , lineHeight = 16.0
114 | , showInfoPanel = False
115 | , wrapParams = { maximumWidth = 55, optimalWidth = 50, stringWidth = String.length }
116 | , wrapOption = DontWrap
117 | }
118 |
119 |
120 | weightedMsgFuzzer : Fuzzer EditorMsg
121 | weightedMsgFuzzer =
122 | Fuzz.frequency
123 | [ ( 1, noOp )
124 | , ( 1, firstLine )
125 | , ( 1, lastLine )
126 | , ( 1, cursorUp )
127 | , ( 1, cursorDown )
128 | , ( 1, cursorLeft )
129 | , ( 1, cursorRight )
130 | , ( 10, insertOne )
131 | , ( 2, insertBlank )
132 | , ( 1, removeCharBefore )
133 | , ( 1, removeCharAfter )
134 | ]
135 |
136 |
137 | noOp : Fuzzer EditorMsg
138 | noOp =
139 | Fuzz.constant NoOp
140 |
141 |
142 | cursorLeft : Fuzzer EditorMsg
143 | cursorLeft =
144 | Fuzz.constant CursorLeft
145 |
146 |
147 | cursorRight : Fuzzer EditorMsg
148 | cursorRight =
149 | Fuzz.constant CursorRight
150 |
151 |
152 | cursorUp : Fuzzer EditorMsg
153 | cursorUp =
154 | Fuzz.constant CursorUp
155 |
156 |
157 | cursorDown : Fuzzer EditorMsg
158 | cursorDown =
159 | Fuzz.constant CursorDown
160 |
161 |
162 | firstLine : Fuzzer EditorMsg
163 | firstLine =
164 | Fuzz.constant FirstLine
165 |
166 |
167 | lastLine : Fuzzer EditorMsg
168 | lastLine =
169 | Fuzz.constant LastLine
170 |
171 |
172 | insertOne : Fuzzer EditorMsg
173 | insertOne =
174 | Fuzz.string |> Fuzz.map oneCharString |> Fuzz.map Insert
175 |
176 |
177 | oneCharString : String -> String
178 | oneCharString str =
179 | case String.left 1 str of
180 | "" ->
181 | " "
182 |
183 | _ ->
184 | str
185 |
186 |
187 | insert : Fuzzer EditorMsg
188 | insert =
189 | Fuzz.string |> Fuzz.map Insert
190 |
191 |
192 | insertBlank : Fuzzer EditorMsg
193 | insertBlank =
194 | Fuzz.constant " " |> Fuzz.map Insert
195 |
196 |
197 | removeCharBefore : Fuzzer EditorMsg
198 | removeCharBefore =
199 | Fuzz.constant RemoveCharBefore
200 |
201 |
202 | removeCharAfter : Fuzzer EditorMsg
203 | removeCharAfter =
204 | Fuzz.constant RemoveCharAfter
205 |
206 |
207 |
208 | --
209 | --hover : Fuzzer Msg
210 | --hover =
211 | -- Fuzz.oneOf
212 | -- [ Fuzz.constant NoHover
213 | -- , Fuzz.map HoverLine Fuzz.int
214 | -- , Fuzz.map2 (\line column -> HoverChar { line = line, column = column }) Fuzz.int Fuzz.int
215 | -- ]
216 | -- |> Fuzz.map Hover
217 | --
218 | --
219 | --goToHoveredPosition : Fuzzer Msg
220 | --goToHoveredPosition =
221 | -- Fuzz.constant GoToHoveredPosition
222 |
--------------------------------------------------------------------------------
/src/Editor/Keymap.elm:
--------------------------------------------------------------------------------
1 | module Editor.Keymap exposing (decoder)
2 |
3 | import Dict exposing (Dict)
4 | import Editor.Update exposing (Msg(..))
5 | import Json.Decode as Decode exposing (Decoder)
6 |
7 |
8 | type Modifier
9 | = None
10 | | Control
11 | | Option
12 | | Shift
13 | | ControlAndShift
14 | | ControlAndOption
15 |
16 |
17 | type alias Keydown =
18 | { char : Maybe String
19 | , key : String
20 | , modifier : Modifier
21 | }
22 |
23 |
24 | modifier : Bool -> Bool -> Bool -> Modifier
25 | modifier ctrl shift option =
26 | case ( ctrl, shift, option ) of
27 | ( True, True, False ) ->
28 | ControlAndShift
29 |
30 | ( False, True, False ) ->
31 | Shift
32 |
33 | ( True, False, False ) ->
34 | Control
35 |
36 | ( False, False, True ) ->
37 | Option
38 |
39 | ( True, False, True ) ->
40 | ControlAndOption
41 |
42 | ( _, _, _ ) ->
43 | None
44 |
45 |
46 | modifierDecoder : Decoder Modifier
47 | modifierDecoder =
48 | Decode.map3 modifier
49 | (Decode.field "ctrlKey" Decode.bool)
50 | (Decode.field "shiftKey" Decode.bool)
51 | (Decode.field "altKey" Decode.bool)
52 |
53 |
54 | characterDecoder : Decoder (Maybe String)
55 | characterDecoder =
56 | Decode.field "key" Decode.string
57 | |> Decode.map
58 | (\key ->
59 | case String.uncons key of
60 | Just ( char, "" ) ->
61 | Just (String.fromChar char)
62 |
63 | _ ->
64 | Nothing
65 | )
66 |
67 |
68 | keydownDecoder : Decoder Keydown
69 | keydownDecoder =
70 | Decode.map3 Keydown
71 | characterDecoder
72 | (Decode.field "key" Decode.string)
73 | modifierDecoder
74 |
75 |
76 | decoder : Decoder Msg
77 | decoder =
78 | keydownDecoder |> Decode.andThen keyToMsg
79 |
80 |
81 | type alias Keymap =
82 | Dict String Msg
83 |
84 |
85 | keymaps :
86 | { noModifier : Keymap
87 | , shift : Keymap
88 | , option : Keymap
89 | , control : Keymap
90 | , controlAndShift : Keymap
91 | , controlAndOption : Keymap
92 | }
93 | keymaps =
94 | { noModifier =
95 | Dict.fromList
96 | [ ( "ArrowUp", CursorUp )
97 | , ( "ArrowDown", CursorDown )
98 | , ( "ArrowLeft", CursorLeft )
99 | , ( "ArrowRight", CursorRight )
100 | , ( "Backspace", RemoveCharBefore )
101 | , ( "Delete", RemoveCharAfter )
102 | , ( "Enter", Insert "\n" )
103 | , ( "Home", CursorToLineStart )
104 | , ( "End", CursorToLineEnd )
105 | , ( "Tab", Indent )
106 | ]
107 | , shift =
108 | Dict.fromList
109 | [ ( "ArrowUp", SelectUp )
110 | , ( "ArrowDown", SelectDown )
111 | , ( "ArrowLeft", SelectLeft )
112 | , ( "ArrowRight", SelectRight )
113 | , ( "Tab", Deindent )
114 | , ( "Home", SelectToLineStart )
115 | , ( "End", SelectToLineEnd )
116 | ]
117 | , option =
118 | Dict.fromList
119 | [ ( "ArrowUp", ScrollUp 20 )
120 | , ( "ArrowDown", ScrollDown 20 )
121 | , ( "ArrowLeft", CursorToLineStart )
122 | , ( "ArrowRight", CursorToLineEnd )
123 | ]
124 | , controlAndOption =
125 | Dict.fromList
126 | [ ( "ArrowUp", FirstLine )
127 | , ( "ArrowDown", LastLine )
128 | , ( "ArrowRight", CursorToGroupEnd )
129 | , ( "ArrowLeft", CursorToGroupStart )
130 | , ( "∑", ToggleWrapping )
131 | , ( "ç", Clear )
132 | ]
133 | , control =
134 | Dict.fromList
135 | [ ( "Backspace", RemoveGroupBefore )
136 | , ( "Delete", RemoveGroupAfter )
137 | , ( "d", SelectGroup )
138 | , ( "c", Copy )
139 | , ( "g", ToggleGoToLinePanel )
140 | , ( ".", RollSearchSelectionForward )
141 | , ( ",", RollSearchSelectionBackward )
142 | , ( "h", ToggleHelp )
143 | , ( "x", Cut )
144 | , ( "s", ToggleSearchPanel )
145 | , ( "r", ToggleReplacePanel )
146 | , ( "v", Paste )
147 | , ( "z", Undo )
148 | , ( "w", WrapSelection )
149 | , ( "y", Redo )
150 | ]
151 | , controlAndShift =
152 | Dict.fromList
153 | [ ( "ArrowRight", SelectToGroupEnd )
154 | , ( "ArrowLeft", SelectToGroupStart )
155 | , ( "C", WriteToSystemClipBoard )
156 | , ( "I", ToggleInfoPanel )
157 | , ( "V", CopyPasteClipboard )
158 | , ( "W", WrapAll )
159 | , ( "S", SendLine )
160 | , ( "A", SelectAll )
161 | ]
162 | }
163 |
164 |
165 | keyToMsg : Keydown -> Decoder Msg
166 | keyToMsg event =
167 | let
168 | keyFrom keymap =
169 | Dict.get event.key keymap
170 | |> Maybe.map Decode.succeed
171 | |> Maybe.withDefault (Decode.fail "This key does nothing")
172 |
173 | keyOrCharFrom keymap =
174 | Decode.oneOf
175 | [ keyFrom keymap
176 | , event.char
177 | |> Maybe.map (Insert >> Decode.succeed)
178 | |> Maybe.withDefault
179 | (Decode.fail "This key does nothing")
180 | ]
181 |
182 | --_ =
183 | -- Debug.log "EV" event
184 | in
185 | case event.modifier of
186 | None ->
187 | keyOrCharFrom keymaps.noModifier
188 |
189 | Control ->
190 | keyFrom keymaps.control
191 |
192 | Shift ->
193 | keyOrCharFrom keymaps.shift
194 |
195 | ControlAndShift ->
196 | keyFrom keymaps.controlAndShift
197 |
198 | ControlAndOption ->
199 | keyFrom keymaps.controlAndOption
200 |
201 | Option ->
202 | keyFrom keymaps.option
203 |
--------------------------------------------------------------------------------
/demo/src/AppText.elm:
--------------------------------------------------------------------------------
1 | module AppText exposing (gettysburgAddress, jabberwocky, long, longLines, tolstoy)
2 |
3 |
4 | gettysburgAddress =
5 | """
6 | Below is Abraham Lincoln's Gettysburg Address.
7 | It was loaded as three long lines. This example
8 | illustrates the current state of the text-wrap
9 | functionality. It is based on Folkert de Vries'
10 | elm-paragraph library.
11 |
12 | Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal.
13 | Now we are engaged in a great civil war, testing whether that nation, or any nation so conceived and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting place for those who here gave their lives that that nation might live. It is altogether fitting and proper that we should do this.
14 |
15 | But, in a larger sense, we can not dedicate—we can not consecrate—we can not hallow—this ground. The brave men, living and dead, who struggled here, have consecrated it, far above our poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living, rather, to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us—that from these honored dead we take increased devotion to that cause for which they gave the last full measure of devotion—that we here highly resolve that these dead shall not have died in vain—that this nation, under God, shall have a new birth of freedom—and that government of the people, by the people, for the people, shall not perish from the earth."""
16 |
17 |
18 | tolstoy =
19 | """“Well, Prince, so Genoa and Lucca are now just family
20 | estates of the Buonapartes. But I warn you, if you don’t
21 | tell me that this means war, if you still try to defend
22 | the infamies and horrors perpetrated by that Antichrist—
23 | I really believe he is Antichrist—I will have nothing
24 | more to do with you and you are no longer my friend,
25 | no longer my ‘faithful slave,’ as you call yourself!
26 | But how do you do? I see I have frightened you—
27 | sit down and tell me all the news.
28 | ”"""
29 |
30 |
31 | longLines =
32 | """
33 | Bioluminescence might seem uncommon, even alien. But biologists think organisms evolved the ability to light up the dark as many as 50 different times, sending tendrils of self-powered luminosity coursing through the tree of life, from fireflies and vampire squids to lantern sharks and foxfire, a fungus found in rotting wood.
34 |
35 | Despite all this diversity, the general rules stay the same. Glowing in the dark or the deep takes two ingredients. You need some sort of luciferin, a molecule that can emit light. And you need an enzyme, luciferase, to trigger that reaction like the snapping of a glowstick.
36 |
37 | Some creatures delegate this chemistry to symbiotic bacteria. Others possess the genes to make their own versions of luciferin and luciferase. But then there’s the golden sweeper, a reef fish that evolved a trick that hasn’t been seen anywhere else, according to a study published Wednesday in Science Advances: It just gobbles up bioluminescent prey and borrows the entire kit.
38 |
39 | “If you can steal an already established, sophisticated system by eating somebody else, that’s way easier,” said Manabu Bessho-Uehara, a postdoctoral scholar at the Monterey Bay Aquarium Research Institute.
40 |
41 | ```elm
42 | update : Buffer -> Msg -> InternalState -> ( InternalState, Buffer, Cmd Msg )
43 | update buffer msg state =
44 | case msg of
45 | NoOp ->
46 | ( state, buffer, Cmd.none )
47 |
48 | FirstLine ->
49 | let
50 | cursor =
51 | { line = 0, column = 0 }
52 |
53 | window =
54 | Window.scrollToIncludeCursor cursor state.window
55 | in
56 | ( { state | cursor = cursor, window = window, selection = Nothing }, buffer, Cmd.none ) |> recordHistory state buffer
57 |
58 | AcceptLineNumber nString ->
59 | case String.toInt nString of
60 | Nothing ->
61 | ( state, buffer, Cmd.none )
62 |
63 | Just n_ ->
64 | let
65 | n =
66 | clamp 0 (List.length (Buffer.lines buffer) - 1) (n_ - 1)
67 |
68 | cursor =
69 | { line = n, column = 0 }
70 |
71 | window =
72 | Window.scrollToIncludeCursor cursor state.window
73 | in
74 | ( { state | cursor = cursor, window = window, selection = Nothing }, buffer, Cmd.none ) |> recordHistory state buffer
75 | ```
76 |
77 |
78 | The End
79 | """
80 |
81 |
82 | long =
83 | List.repeat 30 jabberwocky |> String.join "\n----\n\n"
84 |
85 |
86 | jabberwocky =
87 | """Jabberwocky
88 |
89 | By Lewis Carroll
90 |
91 | ’Twas brillig, and the slithy toves
92 | Did gyre and gimble in the wabe:
93 | All mimsy were the borogoves,
94 | And the mome raths outgrabe.
95 |
96 | “Beware the Jabberwock, my son!
97 | The jaws that bite, the claws that catch!
98 | Beware the Jubjub bird, and shun
99 | The frumious Bandersnatch!”
100 |
101 | He took his vorpal sword in hand;
102 | Long time the manxome foe he sought—
103 | So rested he by the Tumtum tree
104 | And stood awhile in thought.
105 |
106 | And, as in uffish thought he stood,
107 | The Jabberwock, with eyes of flame,
108 | Came whiffling through the tulgey wood,
109 | And burbled as it came!
110 |
111 | One, two! One, two! And through and through
112 | The vorpal blade went snicker-snack!
113 | He left it dead, and with its head
114 | He went galumphing back.
115 |
116 | “And hast thou slain the Jabberwock?
117 | Come to my arms, my beamish boy!
118 | O frabjous day! Callooh! Callay!”
119 | He chortled in his joy.
120 |
121 | ’Twas brillig, and the slithy toves
122 | Did gyre and gimble in the wabe:
123 | All mimsy were the borogoves,
124 | And the mome raths outgrabe.
125 |
126 | PS. Here is the buried treasure.
127 |
128 | NOTES
129 |
130 | 1. The above text about "treasure" is **fake**.
131 | We were just testing to see if we could send
132 | the editor requests like "find the word 'treasure,'
133 | scroll down to it, and highlight it."
134 |
135 | 2. Now that this is working, we have a bit of
136 | code cleanup to do. And more work on some
137 | cursor and highight flakines, e.g., highlighting
138 | should be preserved when scrolling.
139 |
140 |
141 | """
142 |
--------------------------------------------------------------------------------
/src/Editor/Wrap.elm:
--------------------------------------------------------------------------------
1 | module Editor.Wrap exposing (paragraphs, runFSM)
2 |
3 | {-| Code for wrapping text. This needs more thought/work.
4 | -}
5 |
6 | import Dict exposing (Dict)
7 | import Editor.Config exposing (WrapParams)
8 | import Paragraph
9 |
10 |
11 | {-| Wrap text preserving paragraph structure and code blocks
12 | -}
13 | paragraphs : WrapParams -> String -> String
14 | paragraphs wrapParams str =
15 | str
16 | ++ "\n"
17 | |> runFSM
18 | |> List.filter (\( t, s ) -> s /= "")
19 | |> List.map (wrapParagraph wrapParams >> String.trim)
20 | |> String.join "\n\n"
21 |
22 |
23 | {-| Wrap text in paragraph if it is of ParagraphType,
24 | but not if it is code or block
25 | -}
26 | wrapParagraph : WrapParams -> ( ParagraphType, String ) -> String
27 | wrapParagraph wrapParams ( paragraphType, str ) =
28 | case paragraphType of
29 | TextParagraph ->
30 | Paragraph.lines wrapParams str |> String.join "\n"
31 |
32 | CodeParagraph ->
33 | str
34 |
35 | BlockParagraph ->
36 | str
37 |
38 |
39 | {-| Run a finite-state machine that gathers logical paragraphs
40 | into a list of tuples ( ParagraphType, String ) wheree the
41 | first component classifies the type of paragraph (as ParagraphType
42 | or CodeType
43 | -}
44 | runFSM : String -> List ( ParagraphType, String )
45 | runFSM str =
46 | let
47 | lines =
48 | String.lines str
49 |
50 | initialData =
51 | ( Start, { currentParagraph = [], paragraphList = [], tick = 0 } )
52 | in
53 | List.foldl nextState initialData lines
54 | |> Tuple.second
55 | |> .paragraphList
56 | |> List.reverse
57 |
58 |
59 | {-| Then next-state function for the finite-state machine.
60 | -}
61 | nextState : String -> ( State, Data ) -> ( State, Data )
62 | nextState line ( state, data ) =
63 | let
64 | ( newState, action ) =
65 | nextStateAndAction line state
66 | in
67 | ( newState, action line data )
68 |
69 |
70 | {-| A dictionary of functions which carry out the actions
71 | of the finite-state machine.
72 | -}
73 | opDict : Dict String (String -> Data -> Data)
74 | opDict =
75 | Dict.fromList
76 | [ ( "NoOp", \s d -> d )
77 |
78 | --
79 | , ( "StartParagraph", \s d -> { d | currentParagraph = [ s ], tick = d.tick + 1 } )
80 | , ( "AddToParagraph", \s d -> { d | currentParagraph = s :: d.currentParagraph, tick = d.tick + 1 } )
81 | , ( "EndParagraph", \s d -> { d | currentParagraph = [], paragraphList = ( TextParagraph, joinLines d.currentParagraph ) :: d.paragraphList } )
82 |
83 | --
84 | , ( "StartCodeFromBlank", \s d -> { d | currentParagraph = [ s ], paragraphList = ( TextParagraph, joinLines d.currentParagraph ) :: d.paragraphList, tick = d.tick + 1 } )
85 | , ( "StartCodeFromParagraph", \s d -> { d | currentParagraph = [ s ], paragraphList = ( TextParagraph, joinLines d.currentParagraph ) :: d.paragraphList, tick = d.tick + 1 } )
86 | , ( "StartCode", \s d -> { d | currentParagraph = [ s ], tick = d.tick + 1 } )
87 | , ( "AddToCode", \s d -> { d | currentParagraph = s :: d.currentParagraph, tick = d.tick + 1 } )
88 | , ( "EndCode", \s d -> { d | currentParagraph = [], paragraphList = ( CodeParagraph, joinLinesForCode <| s :: d.currentParagraph ) :: d.paragraphList } )
89 |
90 | --
91 | , ( "StartBlockFromBlank", \s d -> { d | currentParagraph = [ s ], paragraphList = ( TextParagraph, joinLines d.currentParagraph ) :: d.paragraphList, tick = d.tick + 1 } )
92 | , ( "StartBlockFromParagraph", \s d -> { d | currentParagraph = [ s ], paragraphList = ( TextParagraph, joinLines d.currentParagraph ) :: d.paragraphList, tick = d.tick + 1 } )
93 | , ( "StartBlock", \s d -> { d | currentParagraph = [ s ], tick = d.tick + 1 } )
94 | , ( "AddToBlock", \s d -> { d | currentParagraph = s :: d.currentParagraph, tick = d.tick + 1 } )
95 | , ( "EndBlock", \s d -> { d | currentParagraph = [], paragraphList = ( BlockParagraph, joinLinesForCode <| s :: d.currentParagraph ) :: d.paragraphList } )
96 | ]
97 |
98 |
99 | {-| Look up the FSM function given its name.
100 | -}
101 | op : String -> (String -> Data -> Data)
102 | op opName =
103 | Dict.get opName opDict |> Maybe.withDefault (\_ d -> d)
104 |
105 |
106 | {-| Join the elements of a string lists with spaces.
107 | -}
108 | joinLines : List String -> String
109 | joinLines list =
110 | list
111 | |> List.reverse
112 | |> List.filter (\s -> s /= "")
113 | |> String.join " "
114 |
115 |
116 | {-| Join the elements of a string lists with newlines.
117 | -}
118 | joinLinesForCode : List String -> String
119 | joinLinesForCode list =
120 | list
121 | |> List.reverse
122 | |> String.join "\n"
123 |
124 |
125 | {-| Define the Finite State Machine
126 | -}
127 | nextStateAndAction : String -> State -> ( State, String -> Data -> Data )
128 | nextStateAndAction line state =
129 | case ( state, classifyLine line ) of
130 | ( InParagraph, Text ) ->
131 | ( InParagraph, op "AddToParagraph" )
132 |
133 | ( InParagraph, Blank ) ->
134 | ( InBlank, op "EndParagraph" )
135 |
136 | ( InParagraph, CodeDelimiter ) ->
137 | ( InCode, op "StartCodeFromParagraph" )
138 |
139 | ( InBlank, Blank ) ->
140 | ( InBlank, op "EndParagraph" )
141 |
142 | ( InBlank, Text ) ->
143 | ( InParagraph, op "StartParagraph" )
144 |
145 | ( InBlank, CodeDelimiter ) ->
146 | ( InCode, op "StartCodeFromBlank" )
147 |
148 | ( Start, Text ) ->
149 | ( InParagraph, op "StartParagraph" )
150 |
151 | ( Start, CodeDelimiter ) ->
152 | ( InCode, op "StartCode" )
153 |
154 | ( Start, Blank ) ->
155 | ( Start, op "NoOp" )
156 |
157 | ( InCode, CodeDelimiter ) ->
158 | ( Start, op "EndCode" )
159 |
160 | ( InCode, Blank ) ->
161 | ( InCode, op "AddToCode" )
162 |
163 | ( InCode, Text ) ->
164 | ( InCode, op "AddToCode" )
165 |
166 | ( InBlank, BeginBlock ) ->
167 | ( InBlock, op "StartBlockFromBlank" )
168 |
169 | ( InParagraph, BeginBlock ) ->
170 | ( InBlock, op "StartBlockFromParagraph" )
171 |
172 | ( Start, BeginBlock ) ->
173 | ( InBlock, op "StartBlock" )
174 |
175 | ( InBlock, EndBlock ) ->
176 | ( Start, op "EndBlock" )
177 |
178 | ( InBlock, Blank ) ->
179 | ( InBlock, op "AddToBlock" )
180 |
181 | ( InBlock, Text ) ->
182 | ( InBlock, op "AddToBlock" )
183 |
184 | ( _, _ ) ->
185 | ( Start, op "NoOp" )
186 |
187 |
188 | type State
189 | = Start
190 | | InParagraph
191 | | InBlank
192 | | InCode
193 | | InBlock
194 |
195 |
196 | type LineType
197 | = Blank
198 | | Text
199 | | CodeDelimiter
200 | | BeginBlock
201 | | EndBlock
202 |
203 |
204 | type ParagraphType
205 | = TextParagraph
206 | | CodeParagraph
207 | | BlockParagraph
208 |
209 |
210 | {-| Classify a line as Blank | CodeDelimiter or Text
211 | -}
212 | classifyLine : String -> LineType
213 | classifyLine str =
214 | let
215 | prefix =
216 | String.trimLeft str
217 | in
218 | if prefix == "" then
219 | Blank
220 |
221 | else if String.left 3 prefix == "```" then
222 | CodeDelimiter
223 |
224 | else if String.left 2 prefix == "$$" then
225 | CodeDelimiter
226 | -- haha
227 |
228 | else if String.left 6 prefix == "\\begin" then
229 | BeginBlock
230 |
231 | else if String.left 4 prefix == "\\end" then
232 | EndBlock
233 |
234 | else
235 | Text
236 |
237 |
238 | {-| The data structure on which the finite-state machine operates.
239 | -}
240 | type alias Data =
241 | { currentParagraph : List String
242 | , paragraphList : List ( ParagraphType, String )
243 | , tick : Int
244 | }
245 |
--------------------------------------------------------------------------------
/demo-simple/src/DemoSimple.elm:
--------------------------------------------------------------------------------
1 | module DemoSimple exposing (Msg(..), main)
2 |
3 | import AppText
4 | import Browser
5 | import Editor exposing (Editor, EditorConfig, EditorMsg)
6 | import Editor.Config exposing (WrapOption(..))
7 | import Editor.Strings
8 | import Html exposing (Html, button, div, text)
9 | import Html.Attributes as HA exposing (style)
10 | import Html.Events exposing (onClick)
11 |
12 |
13 | main : Program () Model Msg
14 | main =
15 | Browser.element
16 | { init = init
17 | , view = view
18 | , update = update
19 | , subscriptions = subscriptions
20 | }
21 |
22 |
23 |
24 | -- INIT
25 |
26 |
27 | type Msg
28 | = EditorMsg EditorMsg
29 | | Test
30 | | FindTreasure
31 | | GetSpeech
32 | | Get500
33 | | Get1000
34 | | Get1500
35 | | Get3000
36 | | Jabberwocky
37 | | Code
38 | | About
39 | | LogErr String
40 |
41 |
42 | type alias Model =
43 | { editor : Editor
44 | , clipboard : String
45 | , document : Document
46 | }
47 |
48 |
49 | type Document
50 | = DocJabberWock
51 | | DocGettysburg
52 | | DocLongLines
53 | | Doc500
54 | | Doc1000
55 | | Doc1500
56 | | Doc3000
57 | | DocAbout
58 | | DocCode
59 |
60 |
61 | init : () -> ( Model, Cmd Msg )
62 | init () =
63 | ( { editor = Editor.init config AppText.about
64 | , clipboard = ""
65 | , document = DocAbout
66 | }
67 | , Cmd.none
68 | )
69 |
70 |
71 | config : EditorConfig Msg
72 | config =
73 | { editorMsg = EditorMsg
74 | , width = 500
75 | , height = 480
76 | , lineHeight = 16.0
77 | , showInfoPanel = True
78 | , wrapParams = { maximumWidth = 55, optimalWidth = 50, stringWidth = String.length }
79 | , wrapOption = DontWrap
80 | , fontProportion = 0.75
81 | , lineHeightFactor = 1.0
82 | }
83 |
84 |
85 |
86 | -- UPDATE
87 |
88 |
89 | update : Msg -> Model -> ( Model, Cmd Msg )
90 | update msg model =
91 | case msg of
92 | EditorMsg editorMsg ->
93 | let
94 | ( editor, cmd ) =
95 | Editor.update editorMsg model.editor
96 | in
97 | ( { model | editor = editor }, Cmd.map EditorMsg cmd )
98 |
99 | Test ->
100 | load DontWrap Editor.Strings.info model
101 |
102 | GetSpeech ->
103 | load DoWrap AppText.gettysburgAddress { model | document = DocGettysburg }
104 |
105 | Get500 ->
106 | load DontWrap AppText.words500 { model | document = Doc500 }
107 |
108 | Get1000 ->
109 | load DontWrap AppText.words1000 { model | document = Doc1000 }
110 |
111 | Get1500 ->
112 | load DontWrap AppText.words1500 { model | document = Doc1500 }
113 |
114 | Get3000 ->
115 | load DontWrap AppText.words3000 { model | document = Doc3000 }
116 |
117 | Jabberwocky ->
118 | load DontWrap AppText.jabberwocky { model | document = DocJabberWock }
119 |
120 | About ->
121 | load DontWrap AppText.about { model | document = DocAbout }
122 |
123 | Code ->
124 | load DontWrap AppText.code { model | document = DocCode }
125 |
126 | FindTreasure ->
127 | highlightText "treasure" model
128 |
129 | LogErr _ ->
130 | ( model, Cmd.none )
131 |
132 |
133 |
134 | -- HELPER FUNCTIONS FOR UPDATE
135 |
136 |
137 | {-| Paste contents of clipboard into Editor
138 | -}
139 | pasteToClipboard : Model -> String -> ( Model, Cmd msg )
140 | pasteToClipboard model editor =
141 | ( { model
142 | | editor =
143 | Editor.insert
144 | (Editor.getWrapOption model.editor)
145 | (Editor.getCursor model.editor)
146 | editor
147 | model.editor
148 | }
149 | , Cmd.none
150 | )
151 |
152 |
153 | pasteToEditorClipboard : Model -> String -> ( Model, Cmd msg )
154 | pasteToEditorClipboard model str =
155 | let
156 | cursor =
157 | Editor.getCursor model.editor
158 |
159 | wrapOption =
160 | Editor.getWrapOption model.editor
161 |
162 | editor2 =
163 | Editor.placeInClipboard str model.editor
164 | in
165 | ( { model | editor = Editor.insert wrapOption cursor str editor2 }, Cmd.none )
166 |
167 |
168 | {-| Load text into Editor
169 | -}
170 | load : WrapOption -> String -> Model -> ( Model, Cmd Msg )
171 | load wrapOption text model =
172 | let
173 | newEditor =
174 | Editor.load wrapOption text model.editor
175 | in
176 | ( { model | editor = newEditor }, Cmd.none )
177 |
178 |
179 | {-| Find str and highlight it
180 | -}
181 | highlightText : String -> Model -> ( Model, Cmd Msg )
182 | highlightText str model =
183 | let
184 | newEditor =
185 | Editor.scrollToString str model.editor
186 | in
187 | ( { model | editor = newEditor }, Cmd.none )
188 |
189 |
190 |
191 | -- SUBSCRIPTIONS
192 |
193 |
194 | subscriptions : Model -> Sub Msg
195 | subscriptions model =
196 | Sub.none
197 |
198 |
199 |
200 | -- VIEW
201 |
202 |
203 | view : Model -> Html Msg
204 | view model =
205 | div
206 | [ HA.style "margin" "60px"
207 | ]
208 | [ title
209 | , div [ HA.style "width" (px <| Editor.getWidth model.editor) ] [ Editor.embedded config model.editor ]
210 | , footer model
211 | ]
212 |
213 |
214 | px : Float -> String
215 | px p =
216 | String.fromFloat p ++ "px"
217 |
218 |
219 | title : Html Msg
220 | title =
221 | div
222 | [ HA.style "font-size" "16px"
223 | , HA.style "font-style" "bold"
224 | , HA.style "margin-bottom" "10px"
225 | ]
226 | [ text "A Pure Elm Text Editor (Simple)" ]
227 |
228 |
229 | footer : Model -> Html Msg
230 | footer model =
231 | div
232 | [ HA.style "font-size" "14px", HA.style "position" "absolute", HA.style "top" "590px", HA.style "left" "80px" ]
233 | [ div []
234 | [ Html.a [ HA.href "https://github.com/jxxcarlson/elm-text-editor" ] [ text "Source code (Work in Progress)" ]
235 | ]
236 | , div [ HA.style "margin-top" "10px" ]
237 | [ text "This editor is based on "
238 | , Html.a [ HA.href "https://sidneynemzer.github.io/elm-text-editor/" ]
239 | [ text "work of Sydney Nemzer" ]
240 | , Html.span [] [ text " and is inspired by previous work of " ]
241 | , Html.a [ HA.href "https://discourse.elm-lang.org/t/text-editor-done-in-pure-elm/1365" ] [ text "Martin Janiczek" ]
242 | ]
243 | , div [ HA.style "margin-top" "10px" ] [ text "ctrl-h to toggle help, ctrl-shift-w to wrap all text" ]
244 | , div [ HA.style "margin-top" "10px" ]
245 | [ aboutButton model
246 | , codeButton model
247 | , jabberWockyButton model
248 | , speechTextButton model
249 | , textButton500 model
250 | , textButton1000 model
251 | , textButton1500 model
252 | , textButton3000 model
253 | ]
254 | ]
255 |
256 |
257 |
258 | -- BUTTONS
259 |
260 |
261 | speechTextButton model =
262 | rowButton model 160 GetSpeech DocGettysburg "Gettysburg Address" []
263 |
264 |
265 | textButton500 model =
266 | rowButton model 100 Get500 Doc500 "500 words" []
267 |
268 |
269 | textButton1000 model =
270 | rowButton model 100 Get1000 Doc1000 "1000 words" []
271 |
272 |
273 | textButton1500 model =
274 | rowButton model 100 Get1500 Doc1500 "1500 words" []
275 |
276 |
277 | textButton3000 model =
278 | rowButton model 100 Get3000 Doc3000 "3000 words" []
279 |
280 |
281 | jabberWockyButton model =
282 | rowButton model 100 Jabberwocky DocJabberWock "Jabberwocky" []
283 |
284 |
285 | aboutButton model =
286 | rowButton model 80 About DocAbout "About" []
287 |
288 |
289 | codeButton model =
290 | rowButton model 80 Code DocCode "Code" []
291 |
292 |
293 | highlight : b -> b -> List (Html.Attribute msg)
294 | highlight source target =
295 | case source == target of
296 | True ->
297 | [ style "background-color" "#900" ]
298 |
299 | False ->
300 | [ style "background-color" "#666" ]
301 |
302 |
303 |
304 | -- STYLE --
305 |
306 |
307 | rowButtonStyle =
308 | [ style "font-size" "12px"
309 | , style "border" "none"
310 | , style "margin-right" "8px"
311 | , style "float" "left"
312 | ]
313 |
314 |
315 | rowButtonLabelStyle width =
316 | [ style "font-size" "12px"
317 | , style "color" "#eee"
318 | , style "width" (String.fromInt width ++ "px")
319 | , style "height" "24px"
320 | , style "border" "none"
321 | ]
322 |
323 |
324 |
325 | -- rowButton : Model -> Int -> b -> Document -> String -> List (Html.Attribute msg) -> Html Msg
326 |
327 |
328 | rowButton model width msg doc str attr =
329 | div (rowButtonStyle ++ attr)
330 | [ button ([ onClick msg ] ++ rowButtonLabelStyle width ++ highlight doc model.document) [ text str ] ]
331 |
--------------------------------------------------------------------------------
/demo-simple/src/AppText.elm:
--------------------------------------------------------------------------------
1 | module AppText exposing (about, code, gettysburgAddress, jabberwocky, long, longLines, tolstoy, words1000, words1500, words3000, words500)
2 |
3 |
4 | about =
5 | """
6 | A bunch of test files here.
7 |
8 | **Main Problems:**
9 |
10 | - Performance: laggy over 1500 words
11 |
12 | """
13 |
14 |
15 | code =
16 | """
17 | {-|
18 |
19 | indexedFilterMap (\\s -> s == "")
20 | ["red" ,"green", "", "", "blue", "", "purple"]
21 | --> [2,3,5] : List Int
22 |
23 | -}
24 | indexedFilterMap : (a -> Bool) -> List a -> List Int
25 | indexedFilterMap filter list =
26 | list
27 | |> List.indexedMap (\\k item -> ( k, item ))
28 | |> List.filter (\\( k, item ) -> filter item)
29 | |> List.map Tuple.first
30 |
31 | {-| Function to find the index of the first
32 | blank line before the index of a given line
33 | -}
34 | selectIndexOfPrecedingParagraph : String -> Int -> Maybe Int
35 | selectIndexOfPrecedingParagraph str end =
36 | let
37 | blankLines_ =
38 | indexedFilterMap
39 | (\\str_ -> str_ == "")
40 | (String.lines str)
41 |
42 | indexOfStart =
43 | List.filter
44 | (\\i -> i < end)
45 | blankLines_ |> List.Extra.last
46 | in
47 | case indexOfStart of
48 | Nothing ->
49 | Nothing
50 |
51 | Just i ->
52 | Just (i + 1)
53 |
54 |
55 | {-| Function to select the paragraph
56 | before the given position
57 | -}
58 | selectPreviousParagraph : Buffer -> Position -> Maybe Position
59 | selectPreviousParagraph (Buffer str) end =
60 | selectIndexOfPrecedingParagraph str end.line
61 | |> Maybe.map (\\line_ -> Position line_ 0)
62 |
63 | """
64 |
65 |
66 | indexedFilterMap : (a -> Bool) -> List a -> List Int
67 | indexedFilterMap filter list =
68 | list
69 | |> List.indexedMap (\k item -> ( k, item ))
70 | |> List.filter (\( k, item ) -> filter item)
71 | |> List.map Tuple.first
72 |
73 |
74 | words500 =
75 | """
76 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut sed turpis est.
77 | Proin vestibulum, nunc ornare auctor vulputate, quam metus porta dolor, a
78 | hendrerit ante ipsum sed ligula. Morbi metus mauris, fermentum dictum blandit
79 | et, malesuada vitae sem. In ante erat, pulvinar nec volutpat ac, volutpat non
80 | risus. Cras elit ligula, volutpat a semper sit amet, ullamcorper fringilla
81 | velit. Vestibulum at nisl vehicula risus egestas faucibus non id elit. Nulla
82 | pharetra vestibulum placerat. Cras ante odio, ullamcorper cursus ante vel,
83 | bibendum suscipit risus. Mauris commodo libero vitae leo cursus, quis hendrerit
84 | orci lobortis.
85 |
86 | In mattis pretium dapibus. Vestibulum ante ipsum primis in faucibus orci luctus
87 | et ultrices posuere cubilia Curae; Suspendisse porta justo a magna dictum
88 | ullamcorper. Donec congue ornare risus sit amet dictum. Phasellus pulvinar vitae
89 | erat et elementum. Pellentesque eget accumsan dui, sed porta diam. Suspendisse
90 | molestie est quis ante vestibulum, et imperdiet odio laoreet. Cras vehicula
91 | risus vel rhoncus laoreet. Nullam laoreet cursus consectetur.
92 |
93 | Proin non mauris in nisi tempor euismod in in mauris. Morbi eu accumsan lacus.
94 | Morbi tincidunt ipsum sit amet lacus vestibulum, non sollicitudin erat
95 | fringilla. Mauris mauris leo, efficitur eu nunc sed, eleifend suscipit nisi.
96 | Cras at augue quis eros mollis fermentum vitae accumsan elit. Aenean interdum
97 | varius elit, scelerisque varius magna consectetur in. In non ex pretium, lacinia
98 | ipsum vel, ullamcorper dolor. Nulla fringilla sagittis venenatis. Suspendisse
99 | aliquam lectus nec leo rutrum laoreet.
100 |
101 | Curabitur ut efficitur erat. Duis ac leo vel massa porttitor sodales. Fusce
102 | interdum leo quis tempus faucibus. Aliquam erat volutpat. Maecenas iaculis
103 | libero lorem. Praesent neque odio, blandit et lobortis eu, ullamcorper a risus.
104 | Fusce pellentesque ligula eget risus maximus laoreet.
105 |
106 | Curabitur tellus neque, malesuada vitae nisi et, volutpat tincidunt mi. Duis
107 | posuere, lacus nec dapibus laoreet, urna odio volutpat enim, et facilisis sem
108 | elit quis diam. Nam orci eros, laoreet eu sagittis et, malesuada et diam.
109 | Suspendisse malesuada nulla in finibus porttitor. Donec consequat felis eu leo
110 | gravida, quis cursus leo auctor. Maecenas libero neque, aliquam ac consequat ut,
111 | ullamcorper id nibh. Fusce semper lobortis tortor, a ornare sapien bibendum vel.
112 | Quisque quis rhoncus justo. Nam vitae feugiat est, eu vehicula turpis. Quisque
113 | bibendum ante quis diam semper, eget tincidunt lacus sagittis. In et justo
114 | lorem. Cras ultrices nisl porttitor placerat efficitur. Suspendisse potenti.
115 |
116 | Aliquam mauris leo, egestas sit amet condimentum ac, imperdiet eu justo. Nulla
117 | quis semper elit. Aenean gravida elementum lectus eget pellentesque. Mauris at
118 | tortor eu nisi porttitor feugiat. Pellentesque vulputate lorem sit amet mattis
119 | iaculis. Curabitur viverra imperdiet odio. Cras ut turpis ut velit hendrerit
120 | congue. Curabitur fringilla, dui et auctor condimentum, tellus nulla varius est,
121 | at finibus tellus magna et dolor. Sed porta suscipit ornare. Praesent
122 | pellentesque ex a porta aliquam. Nunc auctor ullamcorper urna.
123 |
124 | Aliquam erat volutpat. Curabitur auctor sed sem non venenatis. Fusce iaculis
125 | ante a nisi vehicula mattis. Proin facilisis quis sapien eget sodales. Maecenas
126 | pulvinar mauris ut massa pulvinar venenatis. In dignissim lorem ut viverra
127 | scelerisque. Morbi turpis enim, efficitur non pharetra consequat, gravida vitae
128 | neque. Proin lacus augue.
129 |
130 | """
131 |
132 |
133 | words1000 =
134 | words500 ++ words500
135 |
136 |
137 | words1500 =
138 | words1000 ++ words500
139 |
140 |
141 | words3000 =
142 | words1500 ++ words1500
143 |
144 |
145 | gettysburgAddress =
146 | """
147 | Below is Abraham Lincoln's Gettysburg Address.
148 | It was loaded as three long lines. This example
149 | illustrates the current state of the text-wrap
150 | functionality. It is based on Folkert de Vries'
151 | elm-paragraph library.
152 |
153 | Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal.
154 | Now we are engaged in a great civil war, testing whether that nation, or any nation so conceived and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting place for those who here gave their lives that that nation might live. It is altogether fitting and proper that we should do this.
155 |
156 | But, in a larger sense, we can not dedicate—we can not consecrate—we can not hallow—this ground. The brave men, living and dead, who struggled here, have consecrated it, far above our poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living, rather, to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us—that from these honored dead we take increased devotion to that cause for which they gave the last full measure of devotion—that we here highly resolve that these dead shall not have died in vain—that this nation, under God, shall have a new birth of freedom—and that government of the people, by the people, for the people, shall not perish from the earth."""
157 |
158 |
159 | tolstoy =
160 | """“Well, Prince, so Genoa and Lucca are now just family
161 | estates of the Buonapartes. But I warn you, if you don’t
162 | tell me that this means war, if you still try to defend
163 | the infamies and horrors perpetrated by that Antichrist—
164 | I really believe he is Antichrist—I will have nothing
165 | more to do with you and you are no longer my friend,
166 | no longer my ‘faithful slave,’ as you call yourself!
167 | But how do you do? I see I have frightened you—
168 | sit down and tell me all the news.
169 | ”"""
170 |
171 |
172 | longLines =
173 | """
174 | Bioluminescence might seem uncommon, even alien. But biologists think organisms evolved the ability to light up the dark as many as 50 different times, sending tendrils of self-powered luminosity coursing through the tree of life, from fireflies and vampire squids to lantern sharks and foxfire, a fungus found in rotting wood.
175 |
176 | Despite all this diversity, the general rules stay the same. Glowing in the dark or the deep takes two ingredients. You need some sort of luciferin, a molecule that can emit light. And you need an enzyme, luciferase, to trigger that reaction like the snapping of a glowstick.
177 |
178 | Some creatures delegate this chemistry to symbiotic bacteria. Others possess the genes to make their own versions of luciferin and luciferase. But then there’s the golden sweeper, a reef fish that evolved a trick that hasn’t been seen anywhere else, according to a study published Wednesday in Science Advances: It just gobbles up bioluminescent prey and borrows the entire kit.
179 |
180 | “If you can steal an already established, sophisticated system by eating somebody else, that’s way easier,” said Manabu Bessho-Uehara, a postdoctoral scholar at the Monterey Bay Aquarium Research Institute.
181 |
182 | ```elm
183 | update : Buffer -> Msg -> InternalState -> ( InternalState, Buffer, Cmd Msg )
184 | update buffer msg state =
185 | case msg of
186 | NoOp ->
187 | ( state, buffer, Cmd.none )
188 |
189 | FirstLine ->
190 | let
191 | cursor =
192 | { line = 0, column = 0 }
193 |
194 | window =
195 | Window.scrollToIncludeCursor cursor state.window
196 | in
197 | ( { state | cursor = cursor, window = window, selection = Nothing }, buffer, Cmd.none ) |> recordHistory state buffer
198 |
199 | AcceptLineNumber nString ->
200 | case String.toInt nString of
201 | Nothing ->
202 | ( state, buffer, Cmd.none )
203 |
204 | Just n_ ->
205 | let
206 | n =
207 | clamp 0 (List.length (Buffer.lines buffer) - 1) (n_ - 1)
208 |
209 | cursor =
210 | { line = n, column = 0 }
211 |
212 | window =
213 | Window.scrollToIncludeCursor cursor state.window
214 | in
215 | ( { state | cursor = cursor, window = window, selection = Nothing }, buffer, Cmd.none ) |> recordHistory state buffer
216 | ```
217 |
218 |
219 | The End
220 | """
221 |
222 |
223 | long =
224 | List.repeat 30 jabberwocky |> String.join "\n----\n\n"
225 |
226 |
227 | jabberwocky =
228 | """Jabberwocky
229 |
230 | By Lewis Carroll
231 |
232 | ’Twas brillig, and the slithy toves
233 | Did gyre and gimble in the wabe:
234 | All mimsy were the borogoves,
235 | And the mome raths outgrabe.
236 |
237 | “Beware the Jabberwock, my son!
238 | The jaws that bite, the claws that catch!
239 | Beware the Jubjub bird, and shun
240 | The frumious Bandersnatch!”
241 |
242 | He took his vorpal sword in hand;
243 | Long time the manxome foe he sought—
244 | So rested he by the Tumtum tree
245 | And stood awhile in thought.
246 |
247 | And, as in uffish thought he stood,
248 | The Jabberwock, with eyes of flame,
249 | Came whiffling through the tulgey wood,
250 | And burbled as it came!
251 |
252 | One, two! One, two! And through and through
253 | The vorpal blade went snicker-snack!
254 | He left it dead, and with its head
255 | He went galumphing back.
256 |
257 | “And hast thou slain the Jabberwock?
258 | Come to my arms, my beamish boy!
259 | O frabjous day! Callooh! Callay!”
260 | He chortled in his joy.
261 |
262 | ’Twas brillig, and the slithy toves
263 | Did gyre and gimble in the wabe:
264 | All mimsy were the borogoves,
265 | And the mome raths outgrabe.
266 |
267 | PS. Here is the buried treasure.
268 |
269 | NOTES
270 |
271 | 1. The above text about "treasure" is **fake**.
272 | We were just testing to see if we could send
273 | the editor requests like "find the word 'treasure,'
274 | scroll down to it, and highlight it."
275 |
276 | 2. Now that this is working, we have a bit of
277 | code cleanup to do. And more work on some
278 | cursor and highight flakines, e.g., highlighting
279 | should be preserved when scrolling.
280 |
281 |
282 | """
283 |
--------------------------------------------------------------------------------
/src/Editor/View.elm:
--------------------------------------------------------------------------------
1 | module Editor.View exposing (view)
2 |
3 | import Char
4 | import Editor.Config as Config exposing (WrapOption(..))
5 | import Editor.Keymap
6 | import Editor.Model exposing (InternalState)
7 | import Editor.Style as Style
8 | import Editor.Update exposing (Msg(..))
9 | import Editor.Widget as Widget
10 | import Html exposing (Attribute, Html, div, span, text)
11 | import Html.Attributes as Attribute exposing (class, classList, style)
12 | import Html.Events as Event
13 | import Html.Lazy
14 | import Json.Decode as Decode
15 | import List.Extra
16 | import Position exposing (Position)
17 | import RollingList
18 |
19 |
20 | name : String
21 | name =
22 | "elm-editor"
23 |
24 |
25 | selected : Position -> Maybe Position -> Position -> Bool
26 | selected cursor maybeSelection char =
27 | maybeSelection
28 | |> Maybe.map (\selection -> Position.between cursor selection char)
29 | |> Maybe.withDefault False
30 |
31 |
32 | {-| The non-breaking space character will not get whitespace-collapsed like a
33 | regular space.
34 | -}
35 | nonBreakingSpace : Char
36 | nonBreakingSpace =
37 | Char.fromCode 160
38 |
39 |
40 |
41 | {-
42 |
43 | The original code. It incurs a HUGE
44 | performance penalty, resulting in laggy
45 | performance by the editor.
46 |
47 | ensureNonBreakingSpace : Char -> Char
48 | ensureNonBreakingSpace char =
49 | if char == ' ' then
50 | nonBreakingSpace
51 |
52 | else
53 | char
54 |
55 | -}
56 |
57 |
58 | {-| This is Folkert's much better version.
59 | -}
60 | ensureNonBreakingSpace : Char -> Char
61 | ensureNonBreakingSpace char =
62 | case char of
63 | ' ' ->
64 | nonBreakingSpace
65 |
66 | _ ->
67 | char
68 |
69 |
70 | withTrue : a -> ( a, Bool )
71 | withTrue a =
72 | ( a, True )
73 |
74 |
75 | captureOnMouseDown : Msg -> Attribute Msg
76 | captureOnMouseDown msg =
77 | Event.stopPropagationOn
78 | "mousedown"
79 | (Decode.map withTrue (Decode.succeed msg))
80 |
81 |
82 | captureOnMouseOver : Msg -> Attribute Msg
83 | captureOnMouseOver msg =
84 | Event.stopPropagationOn
85 | "mouseover"
86 | (Decode.map withTrue (Decode.succeed msg))
87 |
88 |
89 | character : Position -> Maybe Position -> Position -> Char -> Html Msg
90 | character cursor selection position char =
91 | span
92 | [ classList
93 | [ ( name ++ "-line__character", True )
94 | , ( name ++ "-line__character--has-cursor", cursor == position )
95 | , ( name ++ "-line__character--selected"
96 | , selected cursor selection position
97 | )
98 | ]
99 | , captureOnMouseDown (MouseDown position)
100 | , captureOnMouseOver (MouseOver position)
101 | ]
102 | [ text <| String.fromChar <| ensureNonBreakingSpace char
103 | , if cursor == position then
104 | span [ class <| name ++ "-cursor" ] [ text " " ]
105 |
106 | else
107 | text ""
108 | ]
109 |
110 |
111 | line : Position -> Maybe Position -> Int -> String -> Html Msg
112 | line cursor selection index content =
113 | let
114 | length =
115 | String.length content
116 |
117 | {- Add offset to index to compensate for scrolling -}
118 | endPosition =
119 | { line = index, column = length }
120 | in
121 | div
122 | [ class <| name ++ "-line"
123 | , style "white-space" "pre-wrap"
124 | , captureOnMouseDown (MouseDown endPosition)
125 | , captureOnMouseOver (MouseOver endPosition)
126 | ]
127 | <|
128 | List.concat
129 | [ [ span
130 | [ class <| name ++ "-line__gutter-padding"
131 | , captureOnMouseDown (MouseDown { line = index + 0, column = 0 })
132 | , captureOnMouseOver (MouseOver { line = index + 0, column = 0 })
133 | ]
134 | [ text <| String.fromChar nonBreakingSpace ]
135 | ]
136 | , List.indexedMap
137 | (Position index >> character cursor selection)
138 | (String.toList content)
139 | , if index == cursor.line && cursor.column >= length then
140 | [ span
141 | [ class <| name ++ "-line__character"
142 | , class <| name ++ "-line__character--has-cursor"
143 | ]
144 | [ text " "
145 | , span [ class <| name ++ "-cursor" ] [ text " " ]
146 | ]
147 | ]
148 |
149 | else
150 | []
151 | ]
152 |
153 |
154 | onTripleClick : msg -> Attribute msg
155 | onTripleClick msg =
156 | Event.on
157 | "click"
158 | (Decode.field "detail" Decode.int
159 | |> Decode.andThen
160 | (\detail ->
161 | if detail >= 3 then
162 | Decode.succeed msg
163 |
164 | else
165 | Decode.fail ""
166 | )
167 | )
168 |
169 |
170 | lineNumber : Int -> Html Msg
171 | lineNumber number =
172 | span
173 | [ class <| name ++ "-line-number"
174 | , captureOnMouseDown (MouseDown { line = number, column = 0 })
175 | , captureOnMouseDown SelectLine
176 | , captureOnMouseOver (MouseOver { line = number, column = 0 })
177 | ]
178 | [ text <| String.fromInt (number + 0) ]
179 |
180 |
181 | gutter : Int -> Html Msg
182 | gutter maxLines_ =
183 | -- XXX: Todo: rationalize maxlines
184 | div [ class <| name ++ "-gutter" ] <|
185 | List.map lineNumber (List.range 1 maxLines_)
186 |
187 |
188 | linesContainer : List (Html Msg) -> Html Msg
189 | linesContainer =
190 | div [ class <| name ++ "-lines" ]
191 |
192 |
193 | view : List (Attribute Msg) -> List String -> InternalState -> Html Msg
194 | view attr lines state =
195 | div []
196 | [ div []
197 | [ showIf state.showGoToLinePanel (goToLinePanel state)
198 | , showIf state.showSearchPanel (searchPanel state)
199 | , showIf (not (state.showSearchPanel || state.showGoToLinePanel)) (headerPanel state lines)
200 | ]
201 | , Html.Lazy.lazy3 innerView attr lines state
202 | ]
203 |
204 |
205 | px : Float -> String
206 | px p =
207 | String.fromFloat p ++ "px"
208 |
209 |
210 | innerView : List (Attribute Msg) -> List String -> InternalState -> Html Msg
211 | innerView attr lines state =
212 | div (attr ++ [ Attribute.class "flex-column" ])
213 | [ div
214 | [ class <| name ++ "-container"
215 | , Event.preventDefaultOn
216 | "keydown"
217 | (Decode.map withTrue Editor.Keymap.decoder)
218 | , Event.onMouseUp MouseUp
219 | , Event.onDoubleClick SelectGroup
220 | , onTripleClick SelectLine
221 | , Attribute.tabindex 0
222 | ]
223 | [ gutter (max 100 (List.length lines + 20))
224 | , linesContainer <|
225 | List.indexedMap (line state.cursor state.selection) lines
226 | ]
227 | ]
228 |
229 |
230 | roundTo : Int -> Float -> Float
231 | roundTo places x =
232 | let
233 | pp =
234 | places |> toFloat
235 |
236 | factor =
237 | 10 ^ pp
238 | in
239 | x * factor |> round |> (\u -> toFloat u / factor)
240 |
241 |
242 | currentLineLength : InternalState -> List String -> Html Msg
243 | currentLineLength state lines =
244 | let
245 | lineLength =
246 | List.Extra.getAt state.cursor.line lines
247 | |> Maybe.map (String.length >> String.fromInt)
248 | |> Maybe.withDefault "-1"
249 | in
250 | div [ style "margin-top" "10px" ] [ text <| "Length: " ++ lineLength ]
251 |
252 |
253 | wrappingOption state =
254 | let
255 | message =
256 | if state.config.wrapOption == DoWrap then
257 | "Wrap: ON"
258 |
259 | else
260 | "Wrap: OFF"
261 | in
262 | div [ style "margin-top" "10px" ] [ text message ]
263 |
264 |
265 | infoPanelStyle =
266 | [ style "width" "100px"
267 | , style "position" "fixed"
268 | , style "right" "8px"
269 | , style "top" "8px"
270 | , style "opacity" "1.0"
271 | , style "border" "solid 0.5px #444"
272 | , style "background-color" Style.lightGray
273 | , style "padding" "8px"
274 | , style "z-index" "100"
275 | ]
276 |
277 |
278 | searchPanel state =
279 | if state.showSearchPanel == True then
280 | searchPanel_ state
281 |
282 | else
283 | div [] []
284 |
285 |
286 | headerPanel state lines =
287 | div (headerPanelStyle state.config.width)
288 | [ toggleHelpButtonHeader state, wordCount lines, lineCount lines, wrappingOptionDisplay state, cursorPosition state lines ]
289 |
290 |
291 | toggleHelpButtonHeader state =
292 | let
293 | label =
294 | if state.showHelp == True then
295 | "Help"
296 |
297 | else
298 | "Back"
299 | in
300 | Widget.rowButton 60 ToggleHelp label [ style "height" "25px", style "margin-top" "-3px", style "margin-left" "-30px" ]
301 |
302 |
303 | wrappingOptionDisplay : InternalState -> Html Msg
304 | wrappingOptionDisplay state =
305 | let
306 | message =
307 | if state.config.wrapOption == DoWrap then
308 | "Wrap: ON"
309 |
310 | else
311 | "Wrap: OFF"
312 | in
313 | div (Widget.headingStyle ++ [ style "margin-top" "2px" ]) [ text message ]
314 |
315 |
316 | headerPanelStyle width =
317 | [ style "width" (px (width - 40))
318 | , style "padding-top" "10px"
319 | , style "height" "27px"
320 | , style "background-color" Style.lightGray
321 | , style "opacity" "0.8"
322 | , style "font-size" "14px"
323 | , style "padding-left" "40px"
324 | , Attribute.class "flex-row"
325 | ]
326 |
327 |
328 | searchPanel_ state =
329 | div
330 | [ style "width" "595px"
331 | , style "padding-top" "5px"
332 | , style "height" "30px"
333 | , style "padding-left" "8px"
334 | , style "background-color" Style.lightGray
335 | , style "opacity" "0.9"
336 | , style "font-size" "14px"
337 | , style "float" "left"
338 | ]
339 | [ searchTextButton
340 | , acceptSearchText
341 | , numberOfHitsDisplay state
342 |
343 | -- , syncButton
344 | , showIf (not state.canReplace) openReplaceField
345 | , showIf state.canReplace replaceTextButton
346 | , showIf state.canReplace acceptReplaceText
347 | , searchForwardButton
348 | , searchBackwardButton
349 | , dismissSearchPanel
350 | ]
351 |
352 |
353 | goToLinePanel state =
354 | if state.showGoToLinePanel == True then
355 | goToLinePanel_ state.config.width
356 |
357 | else
358 | div [] []
359 |
360 |
361 | goToLinePanel_ width =
362 | div
363 | [ style "width" (px width)
364 | , style "height" "34px"
365 | , style "padding" "1px"
366 | , style "opacity" "0.9"
367 | , style "background-color" Style.lightGray
368 | , style "float" "left"
369 | ]
370 | [ goToLineButton
371 | , acceptLineNumber
372 | , dismissGoToLineButton
373 | ]
374 |
375 |
376 | dismissGoToLineButton =
377 | Widget.lightRowButton 25
378 | ToggleGoToLinePanel
379 | "X"
380 | [ style "margin-top" "5px"
381 | , style "float" "left"
382 | ]
383 |
384 |
385 | numberOfHitsDisplay : InternalState -> Html Msg
386 | numberOfHitsDisplay state =
387 | let
388 | n =
389 | state.searchResults
390 | |> RollingList.toList
391 | |> List.length
392 |
393 | txt =
394 | String.fromInt (state.searchHitIndex + 1) ++ "/" ++ String.fromInt n
395 | in
396 | Widget.rowButton 40 NoOp txt [ style "float" "left" ]
397 |
398 |
399 | lineCount : List String -> Html Msg
400 | lineCount lines =
401 | div (Widget.headingStyle ++ [ style "margin-top" "2px" ]) [ text ("Lines: " ++ String.fromInt (List.length lines)) ]
402 |
403 |
404 | wordCount : List String -> Html Msg
405 | wordCount lines =
406 | let
407 | words =
408 | List.map String.words lines |> List.concat
409 | in
410 | div (Widget.headingStyle ++ [ style "margin-top" "2px" ]) [ text ("Words: " ++ String.fromInt (List.length words)) ]
411 |
412 |
413 | cursorPosition : InternalState -> List String -> Html Msg
414 | cursorPosition state lines =
415 | let
416 | ll =
417 | List.Extra.getAt state.cursor.line lines
418 | |> Maybe.map (String.length >> String.fromInt)
419 | |> Maybe.withDefault "-1"
420 |
421 | r =
422 | String.fromInt (state.cursor.line + 1)
423 |
424 | c =
425 | String.fromInt state.cursor.column
426 | in
427 | div (Widget.headingStyle ++ [ style "margin-top" "2px" ]) [ text ("Cursor = (" ++ r ++ ", " ++ c ++ ", " ++ ll ++ ")") ]
428 |
429 |
430 |
431 | -- BUTTONS --
432 |
433 |
434 | toggleHelpButton state =
435 | let
436 | label =
437 | if state.showHelp == True then
438 | "Help"
439 |
440 | else
441 | "Back"
442 | in
443 | Widget.columnButton 80 ToggleHelp label []
444 |
445 |
446 | goToLineButton =
447 | Widget.rowButton 80
448 | NoOp
449 | "Go to line"
450 | [ style "margin-top" "5px", style "margin-left" "5px", style "float" "left" ]
451 |
452 |
453 | dismissInfoPanel =
454 | Widget.lightColumnButton 20
455 | ToggleInfoPanel
456 | "X"
457 | []
458 |
459 |
460 | dismissSearchPanel =
461 | Widget.lightRowButton 25
462 | ToggleSearchPanel
463 | "X"
464 | [ style "float" "left", style "float" "left" ]
465 |
466 |
467 | openReplaceField =
468 | Widget.rowButton 25
469 | OpenReplaceField
470 | "R"
471 | []
472 |
473 |
474 | syncButton =
475 | Widget.rowButton 25 SyncToSearchHit "S" [ style "float" "left" ]
476 |
477 |
478 | searchForwardButton =
479 | Widget.rowButton 30 RollSearchSelectionForward ">" [ style "float" "left" ]
480 |
481 |
482 | searchBackwardButton =
483 | Widget.rowButton 30 RollSearchSelectionBackward "<" [ style "float" "left" ]
484 |
485 |
486 | searchTextButton =
487 | Widget.rowButton 60 NoOp "Search" [ style "float" "left" ]
488 |
489 |
490 | replaceTextButton =
491 | Widget.rowButton 70 ReplaceCurrentSelection "Replace" [ style "float" "left" ]
492 |
493 |
494 | acceptLineNumber =
495 | Widget.textField 30
496 | AcceptLineNumber
497 | ""
498 | [ style "margin-top" "5px", style "float" "left" ]
499 | [ setHtmlId "line-number-input" ]
500 |
501 |
502 | acceptSearchText =
503 | Widget.textField 130 AcceptSearchText "" [ style "float" "left" ] [ setHtmlId "editor-search-box" ]
504 |
505 |
506 | acceptReplaceText =
507 | Widget.textField 130 AcceptReplacementText "" [ style "float" "left" ] [ setHtmlId "replacement-box" ]
508 |
509 |
510 | setHtmlId : String -> Html.Attribute msg
511 | setHtmlId id =
512 | Attribute.attribute "id" id
513 |
514 |
515 | showIf : Bool -> Html Msg -> Html Msg
516 | showIf flag h =
517 | if flag then
518 | h
519 |
520 | else
521 | div [] []
522 |
--------------------------------------------------------------------------------
/src/Editor.elm:
--------------------------------------------------------------------------------
1 | module Editor exposing
2 | ( embedded, init
3 | , load, update, insert
4 | , Editor, EditorConfig, EditorMsg
5 | , getBuffer, getState, getSource, getCursor, getWrapOption, getSelectedText, getFontSize, getWidth, transformConfig, lineAt, lineAtCursor
6 | , setSelectedText, setHeight, setWidth
7 | , placeInClipboard
8 | , scrollToLine, scrollToString
9 | )
10 |
11 | {-| Use this module to embed a text editor in an Elm app.
12 |
13 |
14 | ## Contents
15 |
16 | - Embedding the Editor
17 | - Using the Editor
18 | - Types
19 | - Getters
20 | - Clipboard
21 | - Scroll
22 |
23 |
24 | ## Embedding the Editor
25 |
26 | @docs embedded, init
27 |
28 |
29 | ## Using the editor
30 |
31 | @docs load, update, insert
32 |
33 |
34 | ## Types
35 |
36 | @docs Editor, EditorConfig, EditorMsg
37 |
38 |
39 | ## Getters
40 |
41 | @docs getBuffer, getState, getSource, getCursor, getWrapOption, getSelectedText, getFontSize, getWidth, transformConfig, lineAt, lineAtCursor
42 |
43 |
44 | ## Setters
45 |
46 | @docs setSelectedText, setHeight, setWidth
47 |
48 |
49 | ## Clipboard
50 |
51 | @docs placeInClipboard
52 |
53 |
54 | ## Scroll
55 |
56 | @docs scrollToLine, scrollToString
57 |
58 | -}
59 |
60 | import Buffer exposing (Buffer)
61 | import Debounce exposing (Debounce)
62 | import Editor.Config exposing (Config, WrapOption(..), WrapParams)
63 | import Editor.History
64 | import Editor.Model exposing (InternalState)
65 | import Editor.Styles
66 | import Editor.Update
67 | import Editor.View
68 | import Editor.Wrap
69 | import Html exposing (Attribute, Html, div)
70 | import Html.Attributes as HA exposing (style)
71 | import Position exposing (Position)
72 | import RollingList
73 |
74 |
75 | {-| Example:
76 |
77 | type Msg
78 | = EditorMsg EditorMsg
79 | | LogErr String
80 | ...
81 |
82 | -}
83 | type alias EditorMsg =
84 | Editor.Update.Msg
85 |
86 |
87 | {-| Embed the editor in an app like this:
88 |
89 | type alias Model =
90 | { editor : Editor
91 | , ...
92 | }
93 |
94 | -}
95 | type Editor
96 | = Editor
97 | { buffer : Buffer
98 | , state : InternalState
99 | }
100 |
101 |
102 | {-| Get the buffer (mostly for tests)
103 | -}
104 | getBuffer : Editor -> Buffer
105 | getBuffer (Editor data) =
106 | data.buffer
107 |
108 |
109 | {-| Get the state (mostly for tests)
110 | -}
111 | getState : Editor -> InternalState
112 | getState (Editor data) =
113 | data.state
114 |
115 |
116 |
117 | -- GETTERS --
118 |
119 |
120 | {-| -}
121 | getFontSize : Editor -> Float
122 | getFontSize (Editor data) =
123 | data.state.config.fontProportion * data.state.config.lineHeight
124 |
125 |
126 | {-| Get the options for wrapping text. See the example for `insert`.
127 | -}
128 | getWrapOption : Editor -> WrapOption
129 | getWrapOption (Editor data) =
130 | data.state.config.wrapOption
131 |
132 |
133 | {-| Return the the line at the given position
134 | -}
135 | lineAt : Position -> Editor -> Maybe String
136 | lineAt position (Editor data) =
137 | Buffer.lineAt position data.buffer
138 |
139 |
140 | {-| Return the the line at the given position
141 | -}
142 | lineAtCursor : Editor -> String
143 | lineAtCursor (Editor data) =
144 | Buffer.lineAt data.state.cursor data.buffer
145 | |> Maybe.withDefault "invalid cursor"
146 |
147 |
148 | {-| Return the current source text
149 | -}
150 | getSource : Editor -> String
151 | getSource (Editor data) =
152 | Buffer.toString data.buffer
153 |
154 |
155 | {-| Get the cursor position. See the example for `insert`.
156 | -}
157 | getCursor : Editor -> Position
158 | getCursor (Editor data) =
159 | data.state.cursor
160 |
161 |
162 | {-| -}
163 | getSelectedText : Editor -> Maybe String
164 | getSelectedText (Editor data) =
165 | data.state.selectedText
166 |
167 |
168 | {-| -}
169 | getSmallConfig : InternalState -> Config
170 | getSmallConfig s =
171 | s.config
172 |
173 |
174 |
175 | -- SETTERS --
176 |
177 |
178 | {-| -}
179 | setSelectedText : String -> Editor -> Editor
180 | setSelectedText str (Editor data) =
181 | let
182 | is =
183 | data.state
184 | in
185 | Editor { data | state = { is | selectedText = Just str } }
186 |
187 |
188 |
189 | -- CONFIG --
190 |
191 |
192 | {-| A typical configuration:
193 |
194 | config : EditorConfig Msg
195 | config =
196 | { editorMsg = EditorMsg
197 | , sliderMsg = SliderMsg
198 | , editorStyle = editorStyle
199 | , width = 500
200 | , lines = 30
201 | , lineHeight = 16.0
202 | , showInfoPanel = True
203 | , wrapParams = { maximumWidth = 55, optimalWidth = 50, stringWidth = String.length }
204 | , wrapOption = DontWrap
205 | }
206 |
207 | -}
208 | type alias EditorConfig a =
209 | { editorMsg : EditorMsg -> a
210 | , width : Float
211 | , height : Float
212 | , lineHeight : Float
213 | , showInfoPanel : Bool
214 | , wrapParams : { maximumWidth : Int, optimalWidth : Int, stringWidth : String -> Int }
215 | , wrapOption : WrapOption
216 | , fontProportion : Float
217 | , lineHeightFactor : Float
218 | }
219 |
220 |
221 | {-| XXX: Changed
222 | -}
223 | transformConfig : EditorConfig a -> Config
224 | transformConfig c =
225 | let
226 | fontWidth =
227 | c.fontProportion * c.lineHeight
228 |
229 | multiplier =
230 | 1.64
231 | in
232 | { --- lines = floor <| c.height / c.lineHeight
233 | showInfoPanel = c.showInfoPanel
234 | , wrapParams = { maximumWidth = floor (multiplier * c.width / fontWidth - 5), optimalWidth = floor (multiplier * c.width / fontWidth - 10), stringWidth = String.length }
235 | , wrapOption = c.wrapOption
236 | , height = c.height
237 | , width = c.width
238 | , lineHeight = c.lineHeight
239 | , fontProportion = c.fontProportion
240 | , lineHeightFactor = c.lineHeightFactor
241 | }
242 |
243 |
244 |
245 | -- EMBEDDED EDITOR --
246 |
247 |
248 | {-| Embed the editor in the host app:
249 |
250 | view : Model -> Html Msg
251 | view model =
252 | div [ HA.style "margin" "60px" ]
253 | [ ...
254 | , Editor.embedded config model.editor
255 | , ...
256 | ]
257 |
258 | -}
259 | embedded : EditorConfig a -> Editor -> Html a
260 | embedded editorConfig editor =
261 | let
262 | styleConfig =
263 | { editorWidth = editorConfig.width
264 | , editorHeight = editorConfig.height
265 | , lineHeight = editorConfig.lineHeight
266 | , fontProportion = editorConfig.fontProportion
267 | }
268 |
269 | height_ =
270 | editorConfig.height - 37
271 | in
272 | div []
273 | [ Editor.Styles.editorStyles styleConfig
274 | , view (innerStyle height_) editor
275 | |> Html.map editorConfig.editorMsg
276 | ]
277 |
278 |
279 | innerStyle h =
280 | [ style "height" (String.fromFloat h ++ "px")
281 | , style "border" "solid"
282 | , style "border-width" "0.5px"
283 | , style "border-color" "#aaa"
284 | , HA.attribute "id" "__inner_editor__"
285 | , style "overflow-y" "scroll"
286 | , style "height" (String.fromFloat h ++ "px")
287 | ]
288 |
289 |
290 | lines : EditorConfig msg -> Int
291 | lines editorConfig =
292 | floor <| editorConfig.height / editorConfig.lineHeight
293 |
294 |
295 | {-| XXX: Changed
296 |
297 | Initialize the embedded editor:
298 |
299 | init : () -> ( Model, Cmd Msg )
300 | init () =
301 | ( { editor = Editor.init config AppText.jabberwocky
302 | , ...
303 | }
304 | , Cmd.none
305 | )
306 |
307 | -}
308 | init : EditorConfig a -> String -> Editor
309 | init editorConfig text =
310 | Editor
311 | { buffer = Buffer.init text
312 | , state =
313 | { config = transformConfig editorConfig
314 | , topLine = 0
315 | , cursor = Position 0 0
316 | , selection = Nothing
317 | , selectedText = Nothing
318 | , clipboard = ""
319 | , currentLine = Nothing
320 | , dragging = False
321 | , history = Editor.History.empty
322 | , searchTerm = ""
323 | , searchHitIndex = 0
324 | , replacementText = ""
325 | , canReplace = False
326 | , searchResults = RollingList.fromList []
327 | , showHelp = True
328 | , showInfoPanel = editorConfig.showInfoPanel
329 | , showGoToLinePanel = False
330 | , showSearchPanel = False
331 | , savedBuffer = Buffer.fromString ""
332 | , debounce = Debounce.init
333 | }
334 | }
335 |
336 |
337 | {-| Get width in pixels of editor
338 | -}
339 | getWidth : Editor -> Float
340 | getWidth (Editor data) =
341 | data.state.config.width
342 |
343 |
344 | {-| Set width of editor in pixels
345 | -}
346 | setWidth : Float -> Editor -> Editor
347 | setWidth w (Editor data) =
348 | let
349 | oldConfig =
350 | data.state.config
351 |
352 | newConfig =
353 | { oldConfig | width = w }
354 |
355 | oldState =
356 | data.state
357 |
358 | newState =
359 | { oldState | config = newConfig }
360 | in
361 | Editor { data | state = newState }
362 |
363 |
364 | {-| Set height of editor in pixels
365 | -}
366 | setHeight : Float -> Editor -> Editor
367 | setHeight h (Editor data) =
368 | let
369 | oldConfig =
370 | data.state.config
371 |
372 | newConfig =
373 | { oldConfig | height = h }
374 |
375 | oldState =
376 | data.state
377 |
378 | newState =
379 | { oldState | config = newConfig }
380 | in
381 | Editor { data | state = newState }
382 |
383 |
384 | initialState editorConfig =
385 | { config = transformConfig editorConfig
386 | , scrolledLine = 0
387 | , cursor = Position 0 0
388 | , selection = Nothing
389 | , selectedText = Nothing
390 | , clipboard = ""
391 | , currentLine = Nothing
392 | , dragging = False
393 | , history = Editor.History.empty
394 | , searchTerm = ""
395 | , replacementText = ""
396 | , canReplace = False
397 | , searchResults = RollingList.fromList []
398 | , showHelp = True
399 | , showInfoPanel = editorConfig.showInfoPanel
400 | , showGoToLinePanel = False
401 | , showSearchPanel = False
402 | , savedBuffer = Buffer.fromString ""
403 | , debounce = Debounce.init
404 | }
405 |
406 |
407 |
408 | -- UPDATE --
409 |
410 |
411 | {-| Respond to updates in the editor:
412 |
413 | update : Msg -> Model -> ( Model, Cmd Msg )
414 | update msg model =
415 | case msg of
416 | EditorMsg editorMsg ->
417 | let
418 | ( editor, cmd ) =
419 | Editor.update editorMsg model.editor
420 | in
421 | ( { model | editor = editor }, Cmd.map EditorMsg cmd )
422 |
423 | ...
424 |
425 | -}
426 | update : EditorMsg -> Editor -> ( Editor, Cmd EditorMsg )
427 | update msg (Editor data) =
428 | let
429 | ( is, b, cmd ) =
430 | Editor.Update.update data.buffer msg data.state
431 | in
432 | ( Editor { state = is, buffer = b }, cmd )
433 |
434 |
435 |
436 | -- VIEW --
437 |
438 |
439 | {-| xxx
440 | -}
441 | view : List (Attribute EditorMsg) -> Editor -> Html EditorMsg
442 | view attr (Editor data) =
443 | Editor.View.view attr (Buffer.lines data.buffer) data.state
444 |
445 |
446 |
447 | -- EDITOR FUNCTIONS --
448 |
449 |
450 | {-| xxx
451 | -}
452 | wrapSelection : Editor -> Editor
453 | wrapSelection ((Editor data) as editor) =
454 | case data.state.selection of
455 | Nothing ->
456 | editor
457 |
458 | Just sel ->
459 | let
460 | ( start, end ) =
461 | Position.order sel data.state.cursor
462 |
463 | selectedText =
464 | Buffer.between start end data.buffer
465 |
466 | wrappedText =
467 | Editor.Wrap.paragraphs data.state.config.wrapParams selectedText
468 |
469 | oldState =
470 | data.state
471 |
472 | newState =
473 | { oldState | selectedText = Just selectedText }
474 |
475 | newBuffer =
476 | Buffer.replace start end wrappedText data.buffer
477 | in
478 | Editor { state = newState, buffer = newBuffer }
479 |
480 |
481 |
482 | -- ?? -- |> recordHistory state buffer
483 |
484 |
485 | {-| Use to insert text into the editor at a given position, e.g.,
486 |
487 | pasteToClipboard : Model -> String -> ( Model, Cmd msg )
488 | pasteToClipboard model editor =
489 | ( { model
490 | | editor =
491 | Editor.insert
492 | (Editor.getWrapOption model.editor)
493 | (Editor.getCursor model.editor)
494 | editor
495 | model.editor
496 | }
497 | , Cmd.none
498 | )
499 |
500 | -}
501 | insert : WrapOption -> Position -> String -> Editor -> Editor
502 | insert wrapOption position string (Editor data) =
503 | let
504 | textToInsert =
505 | case wrapOption of
506 | DoWrap ->
507 | Editor.Wrap.paragraphs data.state.config.wrapParams string
508 |
509 | DontWrap ->
510 | string
511 | in
512 | Editor { data | buffer = Buffer.insert position textToInsert data.buffer }
513 |
514 |
515 | {-| Place string in the editor's clipboard
516 | -}
517 | placeInClipboard : String -> Editor -> Editor
518 | placeInClipboard str (Editor data) =
519 | let
520 | oldState =
521 | data.state
522 |
523 | newState =
524 | { oldState | clipboard = str }
525 | in
526 | Editor { data | state = newState }
527 |
528 |
529 | {-| xxx
530 | -}
531 | pasteFromClipBoard : Editor -> Editor
532 | pasteFromClipBoard (Editor data) =
533 | Editor { data | buffer = Buffer.insert data.state.cursor data.state.clipboard data.buffer }
534 |
535 |
536 | {-| xxx
537 | -}
538 | clearState : Editor -> Editor
539 | clearState (Editor data) =
540 | Editor { data | state = Editor.Update.clearState data.state }
541 |
542 |
543 | {-| Load text into the embedded editor.
544 |
545 | load : WrapOption -> String -> Model -> ( Model, Cmd Msg )
546 | load wrapOption text model =
547 | let
548 | newEditor =
549 | Editor.load wrapOption text model.editor
550 | in
551 | ( { model | editor = newEditor }, Cmd.none )
552 |
553 | -}
554 | load : WrapOption -> String -> Editor -> Editor
555 | load wrapOption content ((Editor data) as editor) =
556 | let
557 | config =
558 | data.state.config
559 |
560 | lineLengths =
561 | String.lines content |> List.map String.length
562 |
563 | maxLineLength =
564 | List.maximum lineLengths |> Maybe.withDefault 1000
565 |
566 | buffer =
567 | if wrapOption == DoWrap && maxLineLength > config.wrapParams.maximumWidth then
568 | Buffer.fromString (Editor.Wrap.paragraphs config.wrapParams content)
569 |
570 | else
571 | Buffer.fromString content
572 |
573 | (Editor newData) =
574 | clearState editor
575 | in
576 | Editor { newData | buffer = buffer }
577 |
578 |
579 | {-| Scroll the editor to the first occurrence of a given string
580 | -}
581 | scrollToString : String -> Editor -> Editor
582 | scrollToString str (Editor data) =
583 | let
584 | ( is, b ) =
585 | Editor.Update.scrollToText str data.state data.buffer
586 | in
587 | Editor { state = is, buffer = b }
588 |
589 |
590 | {-| Scroll the editor to a given line
591 | -}
592 | scrollToLine : Int -> Editor -> Editor
593 | scrollToLine k (Editor data) =
594 | let
595 | ( is, b ) =
596 | Editor.Update.scrollToLine k data.state data.buffer
597 | in
598 | Editor { state = is, buffer = b }
599 |
--------------------------------------------------------------------------------
/demo/src/Demo.elm:
--------------------------------------------------------------------------------
1 | module Demo exposing (Msg(..), main)
2 |
3 | import Browser
4 | import Browser.Dom as Dom
5 | import Cmd.Extra exposing (withCmd, withCmds, withNoCmd)
6 | import Dict exposing (Dict)
7 | import Editor exposing (Editor, EditorConfig, EditorMsg)
8 | import Editor.Config exposing (WrapOption(..))
9 | import Editor.Update as E
10 | import Html exposing (Attribute, Html, button, div, span, text)
11 | import Html.Attributes as HA exposing (style)
12 | import Html.Events exposing (onClick)
13 | import Json.Encode as E
14 | import Markdown.ElmWithId
15 | import Markdown.Option exposing (..)
16 | import Markdown.Parse as Parse exposing (Id)
17 | import Outside
18 | import Strings
19 | import Task exposing (Task)
20 | import Time
21 | import Tree exposing (Tree)
22 | import Tree.Diff as Diff
23 |
24 |
25 | main : Program Flags Model Msg
26 | main =
27 | Browser.element
28 | { init = init
29 | , view = view
30 | , update = update
31 | , subscriptions = subscriptions
32 | }
33 |
34 |
35 |
36 | -- MSG
37 |
38 |
39 | type Msg
40 | = NoOp
41 | | EditorMsg EditorMsg
42 | | Test
43 | | GotViewport (Result Dom.Error Dom.Viewport)
44 | | Start
45 | | ElmLesson
46 | | MarkdownExample
47 | | MathExample
48 | | ChangeLog
49 | | About
50 | | Outside Outside.InfoForElm
51 | | LogErr String
52 | | SetViewPortForElement (Result Dom.Error ( Dom.Element, Dom.Viewport ))
53 | | Rerender Time.Posix
54 |
55 |
56 | documentDict : Dict String ( Msg, String )
57 | documentDict =
58 | Dict.fromList
59 | [ ( "about", ( About, Strings.about ) )
60 | , ( "elmLesson", ( ElmLesson, Strings.lesson ) )
61 | , ( "changeLog", ( ChangeLog, Strings.changeLog ) )
62 | , ( "markdownExample", ( MarkdownExample, Strings.markdownExample ) )
63 | , ( "mathExample", ( MathExample, Strings.mathExample ) )
64 | , ( "start", ( Start, Strings.test ) )
65 | ]
66 |
67 |
68 | getMsgFromTitle : String -> Msg
69 | getMsgFromTitle title_ =
70 | Dict.get title_ documentDict
71 | |> Maybe.withDefault ( Start, Strings.test )
72 | |> Tuple.first
73 |
74 |
75 |
76 | -- MODEL
77 |
78 |
79 | type alias Model =
80 | { editor : Editor
81 | , clipboard : String
82 | , message : String
83 | , sourceText : String
84 | , ast : Tree Parse.MDBlockWithId
85 | , renderedText : Html Msg
86 | , currentDocumentTitle : String
87 | , width : Float
88 | , height : Float
89 | , counter : Int
90 | , selectedId : Id
91 | }
92 |
93 |
94 | type alias Flags =
95 | { width : Float
96 | , height : Float
97 | }
98 |
99 |
100 | windowProportion =
101 | { width = 0.4
102 | , height = 0.7
103 | }
104 |
105 |
106 | px : Float -> String
107 | px k =
108 | String.fromFloat k ++ "px"
109 |
110 |
111 | init : Flags -> ( Model, Cmd Msg )
112 | init flags =
113 | let
114 | initialText =
115 | Strings.test
116 | in
117 | ( { editor =
118 | Editor.init
119 | { config
120 | | width = windowProportion.width * flags.width
121 | , height = windowProportion.height * flags.height
122 | }
123 | Strings.test
124 | , clipboard = ""
125 | , sourceText = initialText
126 | , ast = Parse.toMDBlockTree 0 ExtendedMath initialText
127 | , renderedText = Markdown.ElmWithId.toHtml ( 0, 0 ) 0 ExtendedMath initialText
128 | , message = "ctrl-h to toggle help"
129 | , currentDocumentTitle = "start"
130 | , width = flags.width
131 | , height = flags.height
132 | , counter = 1
133 | , selectedId = ( 0, 0 )
134 | }
135 | , Cmd.batch [ scrollEditorToTop, scrollRendredTextToTop ]
136 | )
137 |
138 |
139 | config : EditorConfig Msg
140 | config =
141 | { editorMsg = EditorMsg
142 | , width = 450
143 | , height = 544
144 | , lineHeight = 20.0
145 | , showInfoPanel = False
146 | , wrapParams = { maximumWidth = 50, optimalWidth = 45, stringWidth = String.length }
147 | , wrapOption = DoWrap
148 | , fontProportion = 0.75
149 | , lineHeightFactor = 1.0
150 | }
151 |
152 |
153 |
154 | -- UPDATE
155 |
156 |
157 | verticalOffsetInRenderedText =
158 | 160
159 |
160 |
161 | update : Msg -> Model -> ( Model, Cmd Msg )
162 | update msg model =
163 | case msg of
164 | NoOp ->
165 | model |> withNoCmd
166 |
167 | EditorMsg editorMsg ->
168 | let
169 | ( newEditor, editorCmd ) =
170 | Editor.update editorMsg model.editor
171 | in
172 | case editorMsg of
173 | E.CopyPasteClipboard ->
174 | let
175 | clipBoardCmd =
176 | if editorMsg == E.CopyPasteClipboard then
177 | Outside.sendInfo (Outside.AskForClipBoard E.null)
178 |
179 | else
180 | Cmd.none
181 | in
182 | model
183 | |> syncModelWithEditor newEditor
184 | |> withCmds [ clipBoardCmd, Cmd.map EditorMsg editorCmd ]
185 |
186 | E.WriteToSystemClipBoard ->
187 | ( { model | editor = newEditor }, Outside.sendInfo (Outside.WriteToClipBoard (Editor.getSelectedText newEditor |> Maybe.withDefault "Nothing!!")) )
188 |
189 | E.Unload _ ->
190 | syncWithEditor model newEditor editorCmd
191 |
192 | E.RemoveCharAfter ->
193 | syncWithEditor model newEditor editorCmd
194 |
195 | E.RemoveCharBefore ->
196 | syncWithEditor model newEditor editorCmd
197 |
198 | E.Cut ->
199 | syncWithEditor model newEditor editorCmd
200 |
201 | E.Paste ->
202 | syncWithEditor model newEditor editorCmd
203 |
204 | E.Undo ->
205 | syncWithEditor model newEditor editorCmd
206 |
207 | E.Redo ->
208 | syncWithEditor model newEditor editorCmd
209 |
210 | E.WrapSelection ->
211 | syncWithEditor model newEditor editorCmd
212 |
213 | E.Clear ->
214 | syncWithEditor model newEditor editorCmd
215 |
216 | E.WrapAll ->
217 | syncWithEditor model newEditor editorCmd
218 |
219 | E.SendLine ->
220 | syncAndHighlightRenderedText (Editor.lineAtCursor newEditor) (Cmd.map EditorMsg editorCmd) { model | editor = newEditor }
221 |
222 | E.SyncToSearchHit ->
223 | syncAndHighlightRenderedText (Editor.lineAtCursor newEditor) (Cmd.map EditorMsg editorCmd) { model | editor = newEditor }
224 |
225 | _ ->
226 | ( { model | editor = newEditor }, Cmd.map EditorMsg editorCmd )
227 |
228 | SetViewPortForElement result ->
229 | case result of
230 | Ok ( element, viewport ) ->
231 | ( { model | message = "synced" }, setViewPortForSelectedLineInRenderedText verticalOffsetInRenderedText element viewport )
232 |
233 | Err _ ->
234 | ( { model | message = "sync error" }, Cmd.none )
235 |
236 | Start ->
237 | loadDocument "start" model
238 |
239 | Test ->
240 | ( model, Dom.getViewportOf "__inner_editor__" |> Task.attempt GotViewport )
241 |
242 | GotViewport result ->
243 | case result of
244 | Ok vp ->
245 | ( model, Cmd.none )
246 |
247 | Err _ ->
248 | ( model, Cmd.none )
249 |
250 | About ->
251 | loadDocument "about" model
252 |
253 | ElmLesson ->
254 | loadDocument "elmLesson" model
255 |
256 | MarkdownExample ->
257 | loadDocument "markdownExample" model
258 |
259 | MathExample ->
260 | loadDocument "mathExample" model
261 |
262 | ChangeLog ->
263 | loadDocument "changeLog" model
264 |
265 | Outside infoForElm ->
266 | case infoForElm of
267 | Outside.GotClipboard clipboard ->
268 | pasteToEditorClipboard model clipboard
269 |
270 | LogErr _ ->
271 | ( model, Cmd.none )
272 |
273 | Rerender _ ->
274 | let
275 | newSource =
276 | Editor.getSource model.editor
277 | in
278 | { model
279 | | sourceText = newSource
280 | , ast = Parse.toMDBlockTree model.counter ExtendedMath newSource
281 | , renderedText = Markdown.ElmWithId.toHtml model.selectedId model.counter ExtendedMath newSource
282 | }
283 | |> withNoCmd
284 |
285 |
286 |
287 | -- HELPER FUNCTIONS FOR UPDATE
288 |
289 |
290 | syncModelWithEditor : Editor -> Model -> Model
291 | syncModelWithEditor editor model =
292 | let
293 | newSource =
294 | Editor.getSource editor
295 | in
296 | { model
297 | | editor = editor
298 | , counter = model.counter + 2
299 | , sourceText = newSource
300 | , ast = Parse.toMDBlockTree model.counter ExtendedMath newSource
301 | , renderedText = Markdown.ElmWithId.toHtml model.selectedId model.counter ExtendedMath newSource
302 | }
303 |
304 |
305 | syncWithEditor : Model -> Editor -> Cmd EditorMsg -> ( Model, Cmd Msg )
306 | syncWithEditor model editor cmd =
307 | let
308 | newSource =
309 | Editor.getSource editor
310 | in
311 | ( { model
312 | | editor = editor
313 | , counter = model.counter + 2
314 | , sourceText = newSource
315 | , ast = Parse.toMDBlockTree model.counter ExtendedMath newSource
316 | , renderedText = Markdown.ElmWithId.toHtml model.selectedId model.counter ExtendedMath newSource
317 | }
318 | , Cmd.map EditorMsg cmd
319 | )
320 |
321 |
322 | type alias IdRecord =
323 | { id : Int, version : Int }
324 |
325 |
326 | syncAndHighlightRenderedText : String -> Cmd Msg -> Model -> ( Model, Cmd Msg )
327 | syncAndHighlightRenderedText str cmd model =
328 | let
329 | ( _, id_ ) =
330 | Parse.getId (String.trim str) (Parse.sourceMap model.ast)
331 | |> (\( s, i ) -> ( s, i |> Maybe.withDefault "i0v0" ))
332 |
333 | id =
334 | Parse.idFromString id_ |> (\( id__, version ) -> ( id__, version + 1 ))
335 | in
336 | ( processContentForHighlighting model.sourceText { model | selectedId = id }
337 | , Cmd.batch [ cmd, setViewportForElement (Parse.stringFromId id) ]
338 | )
339 |
340 |
341 | processContentForHighlighting : String -> Model -> Model
342 | processContentForHighlighting str model =
343 | let
344 | newAst_ =
345 | Parse.toMDBlockTree model.counter ExtendedMath str
346 |
347 | newAst =
348 | Diff.mergeWith Parse.equalIds model.ast newAst_
349 | in
350 | { model
351 | | sourceText = str
352 |
353 | -- rendering
354 | , ast = newAst
355 | , renderedText = Markdown.ElmWithId.renderHtmlWithTOC model.selectedId "Contents" newAst
356 | , counter = model.counter + 1
357 | }
358 |
359 |
360 | syncRenderedText : String -> Model -> ( Model, Cmd Msg )
361 | syncRenderedText str_ model =
362 | let
363 | ( str, id_ ) =
364 | Parse.getId (String.trim str_) (Parse.sourceMap model.ast)
365 | in
366 | case id_ of
367 | Nothing ->
368 | ( model, Cmd.none )
369 |
370 | Just id ->
371 | ( model, setViewportForElement id )
372 |
373 |
374 | setViewportForElement : String -> Cmd Msg
375 | setViewportForElement id =
376 | Dom.getViewportOf "__rt_scroll__"
377 | |> Task.andThen (\vp -> getElementWithViewPort vp id)
378 | |> Task.attempt SetViewPortForElement
379 |
380 |
381 | scrollEditorToTop =
382 | scrollToTopForElement "__inner_editor__"
383 |
384 |
385 | scrollRendredTextToTop =
386 | scrollToTopForElement "__rt_scroll__"
387 |
388 |
389 | scrollToTopForElement : String -> Cmd Msg
390 | scrollToTopForElement id =
391 | Task.attempt (\_ -> NoOp) (Dom.setViewportOf id 0 0)
392 |
393 |
394 | getElementWithViewPort : Dom.Viewport -> String -> Task Dom.Error ( Dom.Element, Dom.Viewport )
395 | getElementWithViewPort vp id =
396 | Dom.getElement id
397 | |> Task.map (\el -> ( el, vp ))
398 |
399 |
400 | setViewPortForSelectedLineInRenderedText : Float -> Dom.Element -> Dom.Viewport -> Cmd Msg
401 | setViewPortForSelectedLineInRenderedText offset element viewport =
402 | let
403 | y =
404 | viewport.viewport.y + element.element.y - verticalOffsetInRenderedText
405 | in
406 | Task.attempt (\_ -> NoOp) (Dom.setViewportOf "__rt_scroll__" 0 y)
407 |
408 |
409 |
410 | -- COPY-PASTE
411 |
412 |
413 | {-| Paste contents of clipboard into Editor
414 | -}
415 | pasteToClipboard : Model -> String -> ( Model, Cmd msg )
416 | pasteToClipboard model str =
417 | ( { model | editor = Editor.insert (Editor.getWrapOption model.editor) (Editor.getCursor model.editor) str model.editor }, Cmd.none )
418 |
419 |
420 | pasteToEditorClipboard : Model -> String -> ( Model, Cmd Msg )
421 | pasteToEditorClipboard model str =
422 | let
423 | cursor =
424 | Editor.getCursor model.editor
425 |
426 | wrapOption =
427 | Editor.getWrapOption model.editor
428 |
429 | editor2 =
430 | Editor.placeInClipboard str model.editor
431 | in
432 | { model | editor = Editor.insert wrapOption cursor str editor2 }
433 | |> withCmd rerender
434 |
435 |
436 | rerender : Cmd Msg
437 | rerender =
438 | Task.perform Rerender Time.now
439 |
440 |
441 | loadDocument : String -> Model -> ( Model, Cmd Msg )
442 | loadDocument title_ model =
443 | let
444 | ( _, content ) =
445 | Dict.get title_ documentDict |> Maybe.withDefault ( About, Strings.about )
446 |
447 | editor =
448 | Editor.load DoWrap content model.editor
449 |
450 | ast =
451 | Parse.toMDBlockTree model.counter ExtendedMath content
452 |
453 | renderedText =
454 | Markdown.ElmWithId.toHtml model.selectedId model.counter ExtendedMath content
455 | in
456 | ( { model
457 | | counter = model.counter + 1
458 | , editor = editor
459 | , sourceText = content
460 | , ast = ast
461 | , renderedText = renderedText
462 | , currentDocumentTitle = title_
463 | }
464 | , Cmd.batch [ scrollEditorToTop, scrollRendredTextToTop ]
465 | )
466 |
467 |
468 | {-| Load text into Editor
469 | -}
470 | load : WrapOption -> String -> Model -> ( Model, Cmd Msg )
471 | load wrapOption str model =
472 | let
473 | newEditor =
474 | Editor.load wrapOption str model.editor
475 | in
476 | ( { model | editor = newEditor, sourceText = str }, Cmd.none )
477 |
478 |
479 | {-| Find str and highlight it
480 | -}
481 | highlightText : String -> Model -> ( Model, Cmd Msg )
482 | highlightText str model =
483 | let
484 | newEditor =
485 | Editor.scrollToString str model.editor
486 | in
487 | ( { model | editor = newEditor }, Cmd.none )
488 |
489 |
490 |
491 | -- SUBSCRIPTIONS
492 |
493 |
494 | subscriptions : Model -> Sub Msg
495 | subscriptions model =
496 | Sub.batch
497 | [ Outside.getInfo Outside LogErr
498 | ]
499 |
500 |
501 |
502 | -- VIEW
503 |
504 |
505 | view : Model -> Html Msg
506 | view model =
507 | div
508 | [ HA.style "margin-left" "30px"
509 | , HA.class "flex-column"
510 | , HA.style "width" "1200px"
511 | , HA.attribute "id" "__outer_editor__"
512 | ]
513 | [ title
514 | , div
515 | [ HA.class "flex-row"
516 | , HA.style "width" (px model.width)
517 | , HA.style "align-items" "stretch"
518 | ]
519 | [ embeddedEditor model, viewRenderedText model ]
520 | , footer model
521 | ]
522 |
523 |
524 | embeddedEditor : Model -> Html Msg
525 | embeddedEditor model =
526 | -- div [ style "width" (px <| min (maxPaneWidth + 30) (windowProportion.width * model.width + 40)) ]
527 | div [ style "width" (px <| Editor.getWidth model.editor) ]
528 | [ Editor.embedded
529 | { config
530 | | width = min maxPaneWidth (windowProportion.width * model.width + 30)
531 | , height = windowProportion.height * model.height + 24 - 20
532 | }
533 | model.editor
534 | ]
535 |
536 |
537 | maxPaneWidth =
538 | 450
539 |
540 |
541 | viewRenderedText model =
542 | div
543 | [ HA.style "flex" "row"
544 | , HA.style "width" (px <| min maxPaneWidth (windowProportion.width * model.width))
545 | , HA.style "height" (px <| windowProportion.height * model.height - 20)
546 | , HA.style "border" "solid"
547 | , HA.style "border-color" "#444"
548 | , HA.style "border-width" "0.5px"
549 | , HA.style "overflow-y" "scroll"
550 | , HA.style "order" "1"
551 | , HA.style "align-self" "left"
552 | , HA.style "padding" "12px"
553 | , HA.attribute "id" "__rt_scroll__"
554 | ]
555 | [ model.renderedText ]
556 |
557 |
558 | title : Html Msg
559 | title =
560 | div [ HA.style "font-size" "16px", HA.style "font-style" "bold", HA.style "margin-bottom" "10px" ]
561 | [ text "-" ]
562 |
563 |
564 | footer : Model -> Html Msg
565 | footer model =
566 | div
567 | [ HA.style "font-size" "14px", HA.style "margin-top" "16px", HA.class "flex-column" ]
568 | [ div [ HA.style "margin-top" "20px", HA.class "flex-row-text-aligned" ]
569 | [ startButton model, aboutButton model, markdownExampleButton model, mathExampleButton model, elmLessonButton model, changeLogButton model, div [ style "width" "200px", messageColor model.message ] [ text model.message ] ]
570 | , div [ HA.style "margin-top" "10px" ]
571 | [ Html.a [ HA.href "https://github.com/jxxcarlson/elm-text-editor" ] [ text "Source code (Work in Progress)." ]
572 | , text "The editor in this app is based on "
573 | , Html.a [ HA.href "https://sidneynemzer.github.io/elm-text-editor/" ]
574 | [ text "work of Sydney Nemzer" ]
575 | , Html.span [] [ text " and is inspired by previous work of " ]
576 | , Html.a [ HA.href "https://discourse.elm-lang.org/t/text-editor-done-in-pure-elm/1365" ] [ text "Martin Janiczek" ]
577 | ]
578 | ]
579 |
580 |
581 | messageColor : String -> Html.Attribute msg
582 | messageColor str =
583 | case String.contains "error" str of
584 | True ->
585 | HA.style "color" "#a00"
586 |
587 | False ->
588 | HA.style "color" "#444"
589 |
590 |
591 |
592 | -- BUTTONS
593 |
594 |
595 | startButton model =
596 | rowButton model 70 Start "Start" []
597 |
598 |
599 | testButton model =
600 | rowButton model 50 Test "Test" []
601 |
602 |
603 | elmLessonButton model =
604 | rowButton model 50 ElmLesson "Elm" []
605 |
606 |
607 | markdownExampleButton model =
608 | rowButton model 80 MarkdownExample "Markdown" []
609 |
610 |
611 | mathExampleButton model =
612 | rowButton model 70 MathExample "Math" []
613 |
614 |
615 | changeLogButton model =
616 | rowButton model 150 ChangeLog "Issues and Change Log" []
617 |
618 |
619 | aboutButton model =
620 | rowButton model 80 About "About" []
621 |
622 |
623 |
624 | -- STYLE --
625 |
626 |
627 | rowButtonStyle =
628 | [ style "font-size" "12px"
629 | , style "border" "none"
630 | , style "margin-right" "8px"
631 | , style "float" "left"
632 | ]
633 |
634 |
635 | rowButtonLabelStyle width =
636 | [ style "font-size" "12px"
637 | , style "background-color" "#666"
638 | , style "color" "#eee"
639 | , style "width" (String.fromInt width ++ "px")
640 | , style "height" "24px"
641 | , style "border" "none"
642 | , style "margin-right" "10px"
643 | ]
644 |
645 |
646 | activeRowButtonLabelStyle width =
647 | [ style "font-size" "12px"
648 | , style "background-color" "#922"
649 | , style "color" "#eee"
650 | , style "width" (String.fromInt width ++ "px")
651 | , style "height" "24px"
652 | , style "border" "none"
653 | , style "margin-right" "10px"
654 | ]
655 |
656 |
657 | rowButton model width msg str attr =
658 | let
659 | style_ =
660 | case getMsgFromTitle model.currentDocumentTitle == msg of
661 | True ->
662 | activeRowButtonLabelStyle width
663 |
664 | False ->
665 | rowButtonLabelStyle width
666 | in
667 | div (rowButtonStyle ++ attr)
668 | [ button ([ onClick msg ] ++ style_) [ text str ] ]
669 |
670 |
671 |
672 | -- From Simon H:
673 |
674 |
675 | noAttr : Attribute msg
676 | noAttr =
677 | HA.classList []
678 |
679 |
680 | {-| If the condition is true, add the attribute to the element
681 | -}
682 | attrIf : Bool -> Attribute msg -> Attribute msg
683 | attrIf bool attribute =
684 | if bool then
685 | attribute
686 |
687 | else
688 | noAttr
689 |
690 |
691 |
692 | -- ToDo: Use the below
693 | -- div [attrIf condition (style "background" "yellow")] []
694 |
--------------------------------------------------------------------------------