├── .ghci
├── .gitignore
├── .gitmodules
├── CHANGELOG.md
├── LICENSE
├── README.md
├── demo
├── index.html
└── no-docs-available.html
├── dev-scripts
├── build-client.sh
├── client-file-watch.sh
├── css-file-watch.sh
├── generate-css.sh
├── internal
│ └── CompileLucius.hs
├── old
│ ├── container-docker
│ │ └── Dockerfile
│ └── make-container-image.sh
├── run-demo.sh
├── setup-environment.sh
└── soh-runner.sh
├── doc
└── wishlist.md
├── soh-client
├── LICENSE
├── soh-client.cabal
├── soh.lucius
├── src
│ ├── Import.hs
│ ├── Import
│ │ └── Util.hs
│ ├── JavaScript
│ │ ├── Ace.hs
│ │ ├── IFrame.hs
│ │ ├── TermJs.hs
│ │ └── Unmanaged.hs
│ ├── Main.hs
│ ├── Model.hs
│ ├── Model
│ │ ├── Protocol.hs
│ │ └── Server.hs
│ ├── Types.hs
│ ├── View.hs
│ └── View
│ │ ├── Annotation.hs
│ │ ├── Build.hs
│ │ ├── Console.hs
│ │ ├── PosMap.hs
│ │ └── TypeInfo.hs
├── stack.yaml
└── util.js
├── soh-runner-api
├── LICENSE
├── soh-runner-api.cabal
└── src
│ └── SchoolOfHaskell
│ └── Runner
│ └── API.hs
├── soh-runner
├── LICENSE
├── main
│ └── Main.hs
├── soh-runner.cabal
└── src
│ └── SchoolOfHaskell
│ └── Runner.hs
├── soh-scheduler-api
├── LICENSE
├── soh-scheduler-api.cabal
└── src
│ └── SchoolOfHaskell
│ └── Scheduler
│ └── API.hs
├── soh-scheduler
├── .dockerignore
├── Dockerfile
├── LICENSE
├── Makefile
├── Setup.hs
├── main
│ └── Main.hs
├── soh-scheduler.cabal
├── src
│ └── SchoolOfHaskell
│ │ ├── Scheduler.hs
│ │ └── Scheduler
│ │ ├── AWS.hs
│ │ ├── Types.hs
│ │ └── Web.hs
└── stack.yaml
└── stack.yaml
/.ghci:
--------------------------------------------------------------------------------
1 | :set -i.:config:dist/build/autogen
2 | :set -DDEVELOPMENT
3 | :set -XCPP -XTemplateHaskell -XQuasiQuotes -XTypeFamilies -XFlexibleContexts -XGADTs -XOverloadedStrings -XMultiParamTypeClasses -XGeneralizedNewtypeDeriving -XEmptyDataDecls -XDeriveDataTypeable
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist*
2 | static/tmp/
3 | static/combined/
4 | config/client_session_key.aes
5 | *.hi
6 | *.o
7 | *.sqlite3
8 | .hsenv*
9 | cabal-dev/
10 | yesod-devel/
11 | .cabal-sandbox
12 | cabal.sandbox.config
13 | .DS_Store
14 | *.swp
15 | session.*/
16 | .docker-sandbox/
17 | .shake.database
18 | .ssh/
19 | cabal-ghcjs-config
20 | *.js_hi
21 | *.js_o
22 | .stack-work/
23 | dev-scripts/container-docker/ide-backend-server
24 | dev-scripts/container-docker/soh-runner
25 | soh-scheduler/static/soh.js
26 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "deps/ghcjs-react"]
2 | path = deps/ghcjs-react
3 | url = git@github.com:fpco/ghcjs-react.git
4 | [submodule "deps/ghcjs-from-typescript"]
5 | path = deps/ghcjs-from-typescript
6 | url = git://github.com/fpco/ghcjs-from-typescript.git
7 | [submodule "deps/ghcjs-jquery"]
8 | path = deps/ghcjs-jquery
9 | url = git@github.com:ghcjs/ghcjs-jquery.git
10 | [submodule "deps/stack-ide"]
11 | path = deps/stack-ide
12 | url = https://github.com/commercialhaskell/stack-ide
13 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | schoolofhaskell changelog
2 | =========================
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 FP Complete
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # School of Haskell
2 |
3 | The School of Haskell is a service providing interactive Haskell
4 | tutorials and documentation.
5 |
6 | ## How to setup
7 |
8 | Run `./dev-scripts/setup-environment.sh` to install the correct GHC and GHCJS.
9 | Note that GHCJS is currently installed from source, and will take quite a while
10 | to compile and boot.
11 |
12 | ## How to build
13 |
14 | Installation requires stack [stack](https://github.com/commercialhaskell/stack)
15 | 0.1.6 or newer.
16 |
17 | To build the serverside code: `stack build`
18 |
19 | To build the client code and CSS: `./dev-scripts/build-client.sh`
20 |
21 | ## How to run the local demo
22 |
23 | `/dev-scripts/run-demo.sh` will run `soh-runner` and open up a browser window
24 | with the demo. The web snippets in the demo requires `yesod` and
25 | `yesod-media-simple`. Install these with `stack build yesod-media-simple` in
26 | order for the web snippets to compile.
27 |
28 | ## Development tips
29 |
30 | I like to rebuild the client whenever any files change. This is what
31 | `dev-scripts/client-file-watch.sh` does. It requires the
32 | [entr](http://entrproject.org/) file watcher program, which can be installed
33 | with `sudo apt-get install entr`.
34 |
35 | If you just change the lucius file, you can run `./dev-scripts/generate-css.sh`
36 | to regenerate the CSS. `dev-scripts/css-file-watch.sh` is similar to
37 | client-file-watch, but it only rebuilds the CSS.
38 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
45 |
Sierpinski Triangle
46 |
47 | import Diagrams.Backend.Cairo (Cairo)
48 | import Diagrams.Prelude
49 | import Yesod.Media.Simple
50 |
51 | main :: IO ()
52 | main = serve (sierpinski 4)
53 |
54 | sierpinski :: Int -> Diagram Cairo
55 | sierpinski 1 = triangle 1
56 | sierpinski n = s
57 | ===
58 | (s ||| s) # centerX
59 | where s = sierpinski (n-1)
60 |
61 |
Console Calculator
62 |
How would we write a calculator program in Haskell? A simple
63 | calculator should be able to perform the basic arithmetic operations
64 | on two numbers.
65 |
data Operator = Add | Subtract | Multiply | Divide
66 | deriving Read
67 |
68 | eval l o r = case o of
69 | Add -> l + r
70 | Subtract -> l - r
71 | Multiply -> l * r
72 | Divide -> l / r
73 |
74 | prompt txt = do
75 | putStrLn txt
76 | readLn
77 |
78 | main = do
79 | l <- prompt "Left operand?"
80 | o <- prompt "Which operator?"
81 | r <- prompt "Right operand?"
82 | putStrLn $ "The result is " ++ show (eval l o r)
83 |
84 |
Web Calculator
85 |
Let's make our calculator program into a website!
86 |
{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses,
87 | TemplateHaskell, OverloadedStrings #-}
88 | import Yesod hiding (Add, Subtract, Multiply, Divide)
89 | import Yesod.Form
90 | import Control.Applicative
91 |
92 | data Calculator = Calculator
93 |
94 | instance Yesod Calculator
95 |
96 | instance RenderMessage Calculator FormMessage where
97 | renderMessage _ _ = defaultFormMessage
98 |
99 | mkYesod "Calculator" [parseRoutes|
100 | / HomeR GET
101 | |]
102 |
103 | main = warpEnv Calculator
104 |
105 | data Calculation = Calculation Double Operator Double
106 |
107 | form = renderDivs $ Calculation
108 | <$> areq doubleField "Left operand" Nothing
109 | <*> areq (selectField optionsEnum) "Operator" Nothing
110 | <*> areq doubleField "Right operand" Nothing
111 |
112 | getHomeR = do
113 | ((result, widget), enctype) <- runFormGet form
114 |
115 | let resultText = "Result: " ++ case result of
116 | FormSuccess (Calculation l o r) -> show (eval l o r)
117 | _ -> ""
118 |
119 | defaultLayout $ do
120 | [whamlet|
121 | <h1> Calculator </h1>
122 | <form enctype=#{enctype}>
123 | ^{widget}
124 | <input #button type="submit" value="Calculate!">
125 | <h2> #{toHtml resultText}
126 | |]
127 | toWidget [cassius|
128 | body { width: 15em; margin: 0em auto; }
129 | #button { width: 10em; }
130 | input, select { width: 4em; margin: 0.5em; }
131 | .required { text-align: right; }
132 | |]
133 |
134 | data Operator = Add | Subtract | Multiply | Divide
135 | deriving (Eq, Enum, Bounded, Show)
136 |
137 | eval l o r = case o of
138 | Add -> l + r
139 | Subtract -> l - r
140 | Multiply -> l * r
141 | Divide -> l / r
142 |
143 |
144 |
145 |
146 |
--------------------------------------------------------------------------------
/demo/no-docs-available.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | No docs available for selection
4 |
5 |
6 |
--------------------------------------------------------------------------------
/dev-scripts/build-client.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Run this script from the schoolofhaskell directory, like so:
4 | #
5 | # ./dev-scripts/build-client.sh
6 | #
7 | # You can pass stack arguments in as arguments to this script.
8 |
9 | set -xe
10 |
11 | (cd soh-client; stack build --flag soh-client:local-soh-runner $@)
12 | rm -f demo/soh.js
13 | cp $(cd soh-client; stack path --local-install-root)/bin/soh-client.jsexe/all.js demo/soh.js
14 | ./dev-scripts/generate-css.sh
15 |
--------------------------------------------------------------------------------
/dev-scripts/client-file-watch.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | find soh-client -path soh-client/.stack-work -prune -o \( -name *.hs -o -name *.js -o -name *.lucius -o -name *.cabal \) |
4 | entr ./dev-scripts/build-client.sh
5 |
--------------------------------------------------------------------------------
/dev-scripts/css-file-watch.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | find soh-client -path soh-client/.stack-work -prune -o -name *.lucius |
4 | entr ./dev-scripts/generate-css.sh
5 |
--------------------------------------------------------------------------------
/dev-scripts/generate-css.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Run this script from the schoolofhaskell directory, like so:
4 | #
5 | # ./dev-scripts/generate-css.sh
6 |
7 | set -xe
8 |
9 | stack runghc --package shakespeare-2.0.6 dev-scripts/internal/CompileLucius.hs
10 |
--------------------------------------------------------------------------------
/dev-scripts/internal/CompileLucius.hs:
--------------------------------------------------------------------------------
1 | import Text.Lucius
2 | import qualified Data.Text.Lazy as LT
3 | import qualified Data.Text.Lazy.IO as LT
4 |
5 | main = do
6 | input <- LT.readFile "soh-client/soh.lucius"
7 | case luciusRT input [] of
8 | Left err -> fail err
9 | Right res -> LT.writeFile "demo/soh.css" res
10 |
--------------------------------------------------------------------------------
/dev-scripts/old/container-docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM fpco/stack-ghcjs-build:lts-2.14
2 | MAINTAINER Michael Sloan
3 |
4 | RUN cabal update; cabal install ide-backend-rts
5 | ADD soh-runner ide-backend-server /opt/soh/
6 | VOLUME /logs
7 |
8 | ENV PATH /opt/soh:$PATH
9 | ENTRYPOINT ["/opt/soh/soh-runner", "run", "--receipt"]
10 | EXPOSE 3000 4000
11 |
--------------------------------------------------------------------------------
/dev-scripts/old/make-container-image.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -xe
4 |
5 | cd dev-scripts/container-docker
6 |
7 | rm -f soh-runner
8 | rm -f ide-backend-server
9 |
10 | ln -P ../../.stack-work/install/x86_64-linux/lts-2.14/7.8.4/bin/soh-runner soh-runner
11 | ln -P ../../.stack-work/install/x86_64-linux/lts-2.14/7.8.4/bin/ide-backend-server ide-backend-server
12 |
13 | docker build -t mgsloan/soh-runner:local .
14 |
15 | # After this, we can run it via:
16 | #
17 | # docker run -p 127.0.0.1:4000:4000 -t -i mgsloan/soh-ghcjs-dev:local /bin/bash -c 'PATH=/opt/soh/:$PATH /opt/soh/soh-runner'
18 |
--------------------------------------------------------------------------------
/dev-scripts/run-demo.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -x
4 |
5 | sensible-browser demo/index.html &
6 | ./dev-scripts/soh-runner.sh
7 |
--------------------------------------------------------------------------------
/dev-scripts/setup-environment.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -xe
4 |
5 | stack setup
6 | # FIXME: this is needed due to https://github.com/commercialhaskell/stack/issues/1258
7 | stack install happy
8 | (cd soh-client; stack setup)
9 |
--------------------------------------------------------------------------------
/dev-scripts/soh-runner.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -x
4 |
5 | stack exec -- soh-runner run -v -r '00000000-0000-0000-0000-000000000000'
6 |
--------------------------------------------------------------------------------
/doc/wishlist.md:
--------------------------------------------------------------------------------
1 | This is just a braindump wishlist:
2 |
3 | # Type Info Wishlist
4 |
5 | * Dequalify qualified names, and somehow visually make it apparent that this
6 | happened? Hovering over the portion would tell you the full name.
7 |
8 | * Explanations for builtin type syntax. For example, explanations of
9 | forall, (=>), tuples, lists, etc. These could also be made into links
10 | to relevant documentation, though that might bug advanced users (could
11 | make this a setting).
12 |
13 | - Would also apply to highlighted expressions etc, could possibly
14 | also apply to code in the editor.
15 |
16 | * Highlighting of AST nodes of the type on hover. This lets the user
17 | know the structure of the type's AST, and can aid in learning Haskell
18 | syntax. Unfortunately, this is not possible to do perfectly with our
19 | current information, because we need fixities. Even for advanced
20 | users, this will be helpful for understanding types that involve infix
21 | operators.
22 |
23 | - For expressions within the editor, the above is already given by type
24 | info.
25 |
26 | * Allow type synonyms to be replaced by definitions and vice versa.
27 |
28 | - The equivalent in expressions would be inlining and abstraction
29 | refactorings.
30 |
31 | I think the best implementation strategy for the items above is to
32 | expose annotated ASTs from ide-backend, possibly involving
33 | modifications of GHC itself. Until then, we'll stick to the
34 | haskell-src-exts / autocomplete map annotation of type info.
35 |
36 | # Identifier info
37 |
38 | * Fall back on hoogle when identifier info fails. Might be best to implement
39 | this at the stack-ide level so that all clients can benefit.
40 |
--------------------------------------------------------------------------------
/soh-client/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 FP Complete
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/soh-client/soh-client.cabal:
--------------------------------------------------------------------------------
1 | name: soh-client
2 | version: 0.0.1
3 | license: MIT
4 | license-file: LICENSE
5 | author: FP Complete Developers
6 | maintainer: dev@fpcomplete.com
7 | copyright: 2015 FP Complete Corporation
8 | category: Development
9 | build-type: Simple
10 | cabal-version: >=1.10
11 |
12 | Flag local-soh-runner
13 | Description: Use local soh-runner
14 | Default: False
15 |
16 | Flag local-soh-scheduler
17 | Description: Use local soh-scheduler (which will use AWS soh-runners)
18 | Default: False
19 |
20 | executable soh-client
21 | default-language: Haskell2010
22 | hs-source-dirs: src
23 | main-is: Main.hs
24 | js-sources: util.js
25 | ghc-options: -Wall
26 | if flag(local-soh-scheduler)
27 | cpp-options: -DLOCAL_SOH_SCHEDULER
28 | if flag(local-soh-runner)
29 | cpp-options: -DLOCAL_SOH_RUNNER
30 | other-modules: Import
31 | Import.Util
32 | JavaScript.Ace
33 | JavaScript.IFrame
34 | JavaScript.Unmanaged
35 | Model
36 | Model.Protocol
37 | Model.Server
38 | Types
39 | View
40 | View.Build
41 | View.Console
42 | View.PosMap
43 | extensions: CPP
44 | DeriveDataTypeable
45 | DeriveGeneric
46 | FlexibleInstances
47 | ForeignFunctionInterface
48 | GeneralizedNewtypeDeriving
49 | JavaScriptFFI
50 | LambdaCase
51 | MultiParamTypeClasses
52 | NoImplicitPrelude
53 | OverloadedStrings
54 | ParallelListComp
55 | RankNTypes
56 | RecordWildCards
57 | ScopedTypeVariables
58 | StandaloneDeriving
59 | TemplateHaskell
60 | TypeFamilies
61 | ViewPatterns
62 | build-depends: base >=4.7 && <5
63 | , aeson >= 0.8
64 | , async >= 2.0
65 | , base >= 4.7
66 | , bytestring >= 0.10
67 | , ghcjs-ace
68 | , ghcjs-base >= 0.1
69 | , ghcjs-dom >= 0.1
70 | , ghcjs-jquery >= 0.1
71 | , ghcjs-prim >= 0.1
72 | , ghcjs-react
73 | , ghcjs-websockets >= 0.3.0.4
74 | , ide-backend-common
75 | , stack-ide-api
76 | , lens >= 4.7
77 | , mtl >= 2.1
78 | , soh-runner-api
79 | , soh-scheduler-api
80 | , stm >= 2.4
81 | , text >= 1.2
82 | , transformers
83 | , uuid-types >= 1.0
84 | , vector >= 0.10
85 | , void >= 0.7
86 |
--------------------------------------------------------------------------------
/soh-client/soh.lucius:
--------------------------------------------------------------------------------
1 | /* Theme CSS */
2 |
3 | .soh-snippet, #soh-controls > div {
4 | background: #fff;
5 | }
6 |
7 | .soh-snippet, #soh-controls > div, .controls-bar {
8 | border-color: #d1d1d1;
9 | }
10 |
11 | .soh-snippet:hover {
12 | .run {
13 | opacity: 0.75;
14 | color: #c33;
15 | }
16 | }
17 |
18 | .soh-snippet {
19 | .run {
20 | -webkit-transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
21 | -moz-transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
22 | -o-transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
23 | transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
24 | opacity: 0.25;
25 | }
26 | .run:hover {
27 | opacity: 1;
28 | color: #d44;
29 | }
30 | .type-popup {
31 | background: white;
32 | z-index: 1;
33 | border-color: #d1d1d1;
34 | .type-info {
35 | background: initial;
36 | border-color: #d1d1d1;
37 | }
38 | }
39 | .type-info-expr {
40 | background: #e5f0ff;
41 | }
42 | }
43 |
44 | #soh-controls {
45 | .tab {
46 | color: #655;
47 | }
48 | .tab:hover {
49 | background-color: #f1f1f1;
50 | }
51 | .tab.tab-focused {
52 | background-color: #e1e1e1;
53 | /* rounded tab?
54 | background-color: white;
55 | border-bottom: 1px solid white;
56 | border-left: 1px solid #d1d1d1;
57 | border-right: 1px solid #d1d1d1;
58 | position: relative;
59 | top: 1px;
60 | border-radius: 0.5em 0.5em 0 0;
61 | */
62 | }
63 | }
64 | .soh-inline-controls {
65 | margin-top: -1px;
66 | }
67 |
68 | .build-tab-content {
69 | .kind-error {
70 | color: #b94a48;
71 | }
72 | .kind-warning {
73 | color: #9a7a43;
74 | background: #fcf8e3;
75 | }
76 | }
77 |
78 | /* Main CSS */
79 |
80 | #soh-controls > div {
81 | display: none;
82 | }
83 |
84 | #soh-controls > div.soh-visible {
85 | display: block;
86 | }
87 |
88 | .soh-snippet, #soh-controls > div {
89 | font-family: 'Ubuntu', sans-serif;
90 | border-width: 1px;
91 | border-style: solid;
92 | border-radius: 0.2em;
93 | }
94 |
95 | /* Show which snippet is active when the controls aren't attached */
96 | .soh-snippet.soh-current.soh-remote-controls {
97 | border-color: #e1c1c1;
98 | }
99 |
100 | .soh-container {
101 | margin: 0.25em 0 0.75em 0;
102 | }
103 |
104 | .soh-snippet {
105 | width: 800px;
106 | /* same background color as the editor theme */
107 | position: relative;
108 | padding: 0.2em;
109 | .ace_editor {
110 | margin: 0.25em;
111 | height: 300px;
112 | font-size: 15px !important;
113 | }
114 | .run {
115 | position: absolute;
116 | right: 0;
117 | padding: 0.1em;
118 | font-size: 22px;
119 | cursor: pointer;
120 | }
121 | /* glyphicon-play */
122 | .run:before {
123 | content: "\e072";
124 | }
125 | /* glyphicon-cog */
126 | .run.building:before {
127 | content: "\e019";
128 | }
129 | .type-popup {
130 | position: absolute;
131 | border-width: 1px;
132 | border-style: solid;
133 | border-radius: 0.4em;
134 | padding: 0.4em;
135 | max-width: 50%;
136 | .type-info {
137 | padding-bottom: 0.2em;
138 | border-bottom: 1px dashed;
139 | margin-bottom: 0.2em;
140 | background: initial;
141 | }
142 | .type-info:last-child {
143 | padding-bottom: 0;
144 | border-bottom: none;
145 | margin-bottom: 0;
146 | }
147 | }
148 | .type-info-expr {
149 | position: absolute;
150 | }
151 | }
152 |
153 | #soh-controls {
154 | .controls-bar {
155 | height: 2.3em;
156 | border-bottom-width: 1px;
157 | border-bottom-style: solid
158 | }
159 | .tab {
160 | cursor: pointer;
161 | display: block;
162 | float: left;
163 | height: 100%;
164 | font-size: 120%;
165 | padding: 0.2em 0.7em;
166 | }
167 | .soh-close-btn {
168 | cursor: pointer;
169 | display: inline-block;
170 | float: right;
171 | font-size: 150%;
172 | margin-right: 0.25em;
173 | }
174 | .soh-close-btn:before {
175 | content: "X"
176 | }
177 | /* TODO: make it resizable */
178 | .tab-content {
179 | height: 30em;
180 | display: none;
181 | }
182 | .tab-content-focused {
183 | display: block;
184 | }
185 | .hide-mark {
186 | display: none;
187 | }
188 | }
189 |
190 | .docs-link {
191 | cursor: pointer;
192 | }
193 | .docs-link:hover {
194 | text-decoration: underline;
195 | }
196 | .highlighted-haskell {
197 | font-size: 15px !important;
198 | white-space: pre-wrap;
199 | }
200 |
201 | .build-tab-content {
202 | overflow: auto;
203 | padding: 0.25em;
204 | .message {
205 | padding: 0.25em;
206 | min-height: 1.5em;
207 | }
208 | .error-proper-span {
209 | cursor: pointer;
210 | text-decoration: underline;
211 | }
212 | .error-msg {
213 | padding-left: 0.5em;
214 | white-space: pre-wrap;
215 | font-family: 'Ubuntu Mono', monospace;
216 | }
217 | }
218 |
219 | .console-tab-content {
220 | overflow: auto;
221 | background: black;
222 | width: 100%;
223 | height: 100%;
224 | text-align: center;
225 | .terminal {
226 | float: none;
227 | display: inline-block;
228 | border: 2px solid white;
229 | margin-top: 1em;
230 | padding: 1em;
231 | }
232 | }
233 |
234 | .docs-tab-content {
235 | /* This is a hack to make the parent window not scroll when the iframe
236 | * anchor is updated.
237 | */
238 | display: block !important;
239 | position: fixed;
240 | visibility: hidden;
241 | }
242 |
243 | .docs-tab-content.tab-content-focused {
244 | position: initial;
245 | visibility: visible;
246 | }
247 |
248 | .docs-tab-content, .web-tab-content {
249 | iframe {
250 | border: none;
251 | width: 100%;
252 | height: 100%;
253 | }
254 | }
255 |
256 | .tab, .run {
257 | -webkit-user-select: none;
258 | -khtml-user-select: none;
259 | -moz-user-select: none;
260 | -o-user-select: none;
261 | user-select: none;
262 | }
263 |
264 | @-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
265 | @-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
266 | @keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }
267 |
--------------------------------------------------------------------------------
/soh-client/src/Import.hs:
--------------------------------------------------------------------------------
1 | module Import
2 | ( module Control.Applicative
3 | , module Control.Concurrent.STM
4 | , module Control.Lens
5 | , module Control.Monad
6 | , module Data.Foldable
7 | , module Data.Maybe
8 | , module Data.Monoid
9 | , module Data.Traversable
10 | , module GHCJS.Foreign
11 | , module GHCJS.Marshal
12 | , module GHCJS.Types
13 | , module IdeSession.Types.Progress
14 | , module IdeSession.Types.Public
15 | , module Import.Util
16 | , module JavaScript.Unmanaged
17 | , module Prelude
18 | , module React
19 | , module React.Lucid
20 | , module Stack.Ide.JsonAPI
21 | , module Types
22 | , ByteString
23 | , Text
24 | -- * Simplified types
25 | , React
26 | , App
27 | , Component
28 | , UComponent
29 | -- * Misc utils
30 | , ixSnippet
31 | , getEditor
32 | , readEditor
33 | , currentSnippet
34 | , positionControlsOnResize
35 | , schedulerHost
36 | , noDocsUrl
37 | ) where
38 |
39 | import Control.Applicative ((<$>), (<*>))
40 | import Control.Concurrent.STM
41 | import Control.Lens hiding (Sequenced)
42 | import Control.Monad (void, join, when, unless, forever, (>=>), (<=<))
43 | import Data.ByteString (ByteString)
44 | import Data.Foldable (forM_, mapM_)
45 | import Data.Maybe
46 | import Data.Monoid
47 | import Data.Text (Text)
48 | import Data.Traversable (forM, mapM)
49 | import GHCJS.Foreign
50 | import GHCJS.Marshal
51 | import GHCJS.Types
52 | import IdeSession.Types.Progress
53 | import IdeSession.Types.Public
54 | import Import.Util
55 | import JavaScript.Ace (Editor)
56 | import JavaScript.Unmanaged
57 | import Prelude hiding (mapM, mapM_)
58 | import React hiding (App, getElementById)
59 | import qualified React.Internal
60 | import React.Lucid
61 | import Stack.Ide.JsonAPI
62 | import Types
63 |
64 | type React a = ReactT State IO a
65 |
66 | type App = React.Internal.App State IO
67 |
68 | type Component a = React.Internal.Component State a IO
69 |
70 | type UComponent a = React.Internal.Component State (Unmanaged a) IO
71 |
72 | ixSnippet :: SnippetId -> Traversal' State Snippet
73 | ixSnippet (SnippetId sid) = stateSnippets . ix sid
74 |
75 | getEditor :: State -> SnippetId -> IO Editor
76 | getEditor state sid =
77 | getUnmanagedOrFail (state ^? ixSnippet sid . snippetEditor)
78 |
79 | readEditor :: TVar State -> SnippetId -> IO Editor
80 | readEditor stateVar sid =
81 | readUnmanagedOrFail stateVar (^? ixSnippet sid . snippetEditor)
82 |
83 | currentSnippet :: State -> Maybe SnippetId
84 | currentSnippet state =
85 | case state ^. stateStatus of
86 | InitialStatus -> Nothing
87 | BuildRequested (BuildRequest sid _) -> Just sid
88 | Building sid _ -> Just sid
89 | Built sid _ -> Just sid
90 | QueryRequested sid _ _ -> Just sid
91 | KillRequested sid _ -> Just sid
92 |
93 | foreign import javascript unsafe
94 | "positionControlsOnResize"
95 | positionControlsOnResize :: Element -> Element -> IO ()
96 |
97 | schedulerHost :: Text
98 | #if LOCAL_SOH_SCHEDULER
99 | schedulerHost = "http://localhost:3000"
100 | #else
101 | schedulerHost = "http://soh-scheduler-1627848338.us-east-1.elb.amazonaws.com"
102 | #endif
103 |
104 | noDocsUrl :: Text
105 | --FIXME
106 | -- noDocsUrl = schedulerHost <> "/static/no-docs-available.html"
107 | noDocsUrl = "no-docs-available.html"
108 |
--------------------------------------------------------------------------------
/soh-client/src/Import/Util.hs:
--------------------------------------------------------------------------------
1 | {-# OPTIONS_GHC -fno-warn-orphans #-}
2 |
3 | module Import.Util where
4 |
5 | import Control.Concurrent.STM
6 | import Control.Exception (SomeException, catch, throwIO)
7 | import Control.Lens hiding (Sequenced, coerce)
8 | import Control.Monad (unless, (<=<))
9 | import Control.Monad.Trans.Maybe (MaybeT(..))
10 | import Data.Char (isHexDigit)
11 | import Data.Coerce (coerce)
12 | import Data.Foldable (forM_)
13 | import Data.IORef
14 | import Data.Monoid
15 | import Data.Text (Text, pack)
16 | import qualified Data.Text as T
17 | import Data.Typeable (Typeable, typeRep)
18 | import GHCJS.DOM.HTMLElement (HTMLElement)
19 | import GHCJS.DOM.Types (IsElement)
20 | import GHCJS.Foreign
21 | import GHCJS.Marshal
22 | import GHCJS.Types
23 | import IdeSession.Types.Public
24 | import JavaScript.JQuery (JQuery, selectElement)
25 | import JavaScript.JQuery.Internal (jq_getText)
26 | import Prelude
27 | import React
28 | import React.Lucid
29 | import SchoolOfHaskell.Runner.API
30 | import Stack.Ide.JsonAPI
31 | import System.IO.Unsafe (unsafePerformIO)
32 |
33 | addWhen :: Bool -> Text -> Text -> Text
34 | addWhen True x y = y <> " " <> x
35 | addWhen False _ y = y
36 |
37 | tshow :: Show a => a -> Text
38 | tshow = pack . show
39 |
40 | once :: IO () -> IO (IO ())
41 | once runAction = do
42 | alreadyCalledRef <- newIORef False
43 | return $ do
44 | alreadyCalled <- readIORef alreadyCalledRef
45 | unless alreadyCalled runAction
46 |
47 | debounce :: Int -> IO () -> IO (IO ())
48 | debounce ms f = do
49 | f' <- asyncCallback AlwaysRetain f
50 | mtimeoutRef <- newIORef Nothing
51 | return $ do
52 | mtimeout <- readIORef mtimeoutRef
53 | forM_ mtimeout clearTimeout
54 | writeIORef mtimeoutRef . Just =<< setTimeout f' ms
55 |
56 | newtype TimeoutId = TimeoutId (JSRef TimeoutId)
57 | deriving (Typeable, ToJSRef, FromJSRef)
58 |
59 | foreign import javascript unsafe "setTimeout($1, $2)"
60 | setTimeout :: JSFun (IO ()) -> Int -> IO TimeoutId
61 |
62 | foreign import javascript unsafe "clearTimeout($1)"
63 | clearTimeout :: TimeoutId -> IO ()
64 |
65 | --------------------------------------------------------------------------------
66 | -- Misc utils
67 |
68 | prop :: FromJSRef a => JSRef obj -> JSString -> IO (Maybe a)
69 | prop obj n = do
70 | ref <- getProp n obj
71 | if isUndefined ref || isNull ref
72 | then return Nothing
73 | else fromJSRef ref
74 |
75 | mtprop :: FromJSRef a => JSRef obj -> JSString -> MaybeT IO a
76 | mtprop obj n = MaybeT $ prop obj n
77 |
78 | expectProp :: FromJSRef a => JSRef obj -> JSString -> IO a
79 | expectProp obj n = do
80 | mx <- prop obj n
81 | case mx of
82 | Nothing -> fail $ "Couldn't find expected property " ++ fromJSString n
83 | Just x -> return x
84 |
85 | setPropShow :: (Monad m, Show a) => T.Text -> a -> ReactT state m ()
86 | setPropShow n = attr n . T.pack . show
87 |
88 | divClass :: Monad m => Text -> ReactT state m a -> ReactT state m a
89 | divClass className f = div_ $ do
90 | class_ className
91 | f
92 |
93 | spanClass :: Monad m => Text -> ReactT state m a -> ReactT state m a
94 | spanClass className f = span_ $ do
95 | class_ className
96 | f
97 |
98 | foreign import javascript unsafe "$2.get($1)"
99 | getElement :: Int -> JQuery -> IO HTMLElement
100 |
101 | -- TODO: Find a better way to do this, or add similar utilities to ghcjs-base
102 | intToJSNumber :: Int -> JSNumber
103 | intToJSNumber n = coerce $ unsafePerformIO $ toJSRef n
104 |
105 | fromJSRefOrFail :: (FromJSRef a, Typeable a) => JSRef a -> IO a
106 | fromJSRefOrFail ref = fromJSRefOrFail' (show (typeRep ref)) ref
107 |
108 | fromJSRefOrFail' :: FromJSRef a => String -> JSRef a -> IO a
109 | fromJSRefOrFail' what ref = do
110 | mx <- fromJSRef ref
111 | case mx of
112 | Nothing -> fail $ "Failed to marshal " ++ what
113 | Just x -> return x
114 |
115 | getElementsByClassName :: JSString -> IO [Element]
116 | getElementsByClassName name =
117 | mapM fromJSRefOrFail =<< fromArray =<< getElementsByClassName' name
118 |
119 | foreign import javascript unsafe "document.getElementsByClassName($1)"
120 | getElementsByClassName' :: JSString -> IO (JSArray Element)
121 |
122 | getElementById :: JSString -> IO (Maybe Element)
123 | getElementById name =
124 | fromJSRef =<< getElementById' name
125 |
126 | foreign import javascript unsafe "document.getElementById($1)"
127 | getElementById' :: JSString -> IO (JSRef Element)
128 |
129 | getElementText :: IsElement e => e -> IO JSString
130 | getElementText = jq_getText <=< selectElement
131 |
132 | --------------------------------------------------------------------------------
133 | -- Tvar/lens helpers
134 |
135 | setTVarIO :: TVar s -> ASetter' s a -> a -> IO ()
136 | setTVarIO v l a = atomically $ modifyTVar' v $ set l a
137 |
138 | modifyTVarIO :: TVar s -> ASetter' s a -> (a -> a) -> IO ()
139 | modifyTVarIO v l f = atomically $ modifyTVar' v $ over l f
140 |
141 | waitForTVarIO :: TVar s -> (s -> Maybe a) -> IO a
142 | waitForTVarIO v f = atomically $ do
143 | x <- readTVar v
144 | case f x of
145 | Just y -> return y
146 | Nothing -> retry
147 |
148 | viewTVarIO :: TVar s -> Getter s a -> IO a
149 | viewTVarIO v g =
150 | atomically
151 | (fmap (view g)
152 | (readTVar v))
153 |
154 | foreign import javascript "console.log($1)" consoleLog :: JSRef a -> IO ()
155 |
156 | foreign import javascript "console.warn($1)" consoleWarn :: JSRef a -> IO ()
157 |
158 | foreign import javascript "console.error($1)" consoleError :: JSRef a -> IO ()
159 |
160 | consoleLogText :: Text -> IO ()
161 | consoleLogText = consoleLog . toJSString
162 |
163 | consoleWarnText :: Text -> IO ()
164 | consoleWarnText = consoleWarn . toJSString
165 |
166 | consoleErrorText :: Text -> IO ()
167 | consoleErrorText = consoleError . toJSString
168 |
169 | showExceptions :: Text -> IO a -> IO a
170 | showExceptions msg f = f `catch` \ex -> do
171 | consoleErrorText $ "Exception in " <> msg <> ": " <> tshow (ex :: SomeException)
172 | throwIO ex
173 |
174 | showAndIgnoreExceptions :: Text -> IO () -> IO ()
175 | showAndIgnoreExceptions msg f = f `catch` \ex ->
176 | consoleErrorText $
177 | "Exception ignored in " <> msg <> ": " <> tshow (ex :: SomeException)
178 |
179 | --------------------------------------------------------------------------------
180 | -- JsonAPI helpers
181 |
182 | displayIdInfo :: IdInfo -> Text
183 | displayIdInfo (IdInfo IdProp {..} scope) =
184 | "'" <> idName <> "' " <> displayNameSpace idSpace <> " " <>
185 | case scope of
186 | Binder -> "binding"
187 | Local -> "defined locally" <>
188 | case idDefSpan of
189 | ProperSpan ss -> ", at " <> tshow ss
190 | _ -> ""
191 | Imported {..} ->
192 | "imported from " <> displayModuleId idImportedFrom <>
193 | (if idDefinedIn /= idImportedFrom
194 | then ", and defined in " <> displayModuleId idDefinedIn
195 | else "")
196 | WiredIn -> "builtin defined in " <> displayModuleId idDefinedIn
197 |
198 | displayNameSpace :: IdNameSpace -> Text
199 | displayNameSpace VarName = "value"
200 | displayNameSpace DataName = "data constructor"
201 | displayNameSpace TvName = "type variable"
202 | displayNameSpace TcClsName = "type"
203 |
204 | displayModuleId :: ModuleId -> Text
205 | displayModuleId (ModuleId mo pkg) = mo <> " (" <> displayPackageId pkg <> ")"
206 |
207 | displayPackageId :: PackageId -> Text
208 | displayPackageId (PackageId name (Just version) _pkey) =
209 | name <> "-" <> cleanPackageVersion version
210 | displayPackageId (PackageId name Nothing _pkey) =
211 | name
212 |
213 | -- | In our environment, ghc-prim / base / etc has a name like this
214 | --
215 | -- ghc-prim-0.3.1.0-3f9f683cd77bf581e40e5d3162876874
216 | --
217 | -- It seems like a good hack for now to just strip the "-hash" off any
218 | -- such package versions. Consider moving it to ide-backend-client?
219 | cleanPackageVersion :: Text -> Text
220 | cleanPackageVersion x@(T.stripPrefix "-" . T.takeEnd 33 -> Just hash)
221 | | T.all isHexDigit hash = T.dropEnd 33 x
222 | cleanPackageVersion x = x
223 |
224 | -- Orphans
225 |
226 | $(concat <$> mapM makePrisms [''Response, ''RunnerResponse, ''Sequenced])
227 |
228 | deriving instance Typeable Element
229 |
--------------------------------------------------------------------------------
/soh-client/src/JavaScript/Ace.hs:
--------------------------------------------------------------------------------
1 | -- | Bindings to the Ace editor.
2 | module JavaScript.Ace
3 | ( Editor
4 | -- * Construction
5 | , makeEditor
6 | -- * Queries
7 | , getValue
8 | , getSelection
9 | , getCharPosition
10 | -- * Mutations
11 | , setValue
12 | , setSelection
13 | , Editor.focus
14 | , Editor.blur
15 | , setMaxLinesInfty
16 | , MarkerId(..)
17 | , addMarker
18 | , removeMarker
19 | -- * Events
20 | , onChange
21 | , ChangeEvent(..)
22 | , onSelectionChange
23 | , addCommand
24 | -- * Auxiliary Types
25 | , Pos(..)
26 | , Range(..)
27 | , Selection(..)
28 | , selectionToRange
29 | , rangeToSelection
30 | , RangeOrdering(..)
31 | , compareRange
32 | , comparePosWithRange
33 | , DeltaPos(..)
34 | , subtractPos
35 | , shiftPos
36 | , shiftRange
37 | ) where
38 |
39 | import Control.Monad (void, join, (<=<))
40 | import Control.Monad.Trans.Maybe (MaybeT(..))
41 | import Data.Coerce (coerce)
42 | import Data.Text (Text)
43 | import Data.Typeable (Typeable)
44 | import GHC.Generics (Generic)
45 | import GHCJS.DOM.Types (HTMLElement(..))
46 | import GHCJS.Foreign
47 | import GHCJS.Marshal
48 | import GHCJS.Types
49 | import Import.Util (getElement, intToJSNumber, mtprop, fromJSRefOrFail, fromJSRefOrFail')
50 | import qualified JavaScript.AceAjax.Raw.Ace as Ace
51 | import qualified JavaScript.AceAjax.Raw.Editor as Editor
52 | import qualified JavaScript.AceAjax.Raw.IEditSession as Session
53 | import qualified JavaScript.AceAjax.Raw.Selection as Selection
54 | import JavaScript.AceAjax.Raw.Types hiding (Selection, Range)
55 | import qualified JavaScript.AceAjax.Raw.VirtualRenderer as Renderer
56 | import JavaScript.JQuery (JQuery)
57 | import Prelude
58 |
59 | --------------------------------------------------------------------------------
60 | -- Construction
61 |
62 | makeEditor :: JQuery -> IO Editor
63 | makeEditor q = do
64 | e <- join $ Ace.edit1 <$> ace <*> getElement 0 q
65 | Editor.setHighlightActiveLine e (toJSBool False)
66 | s <- Editor.session e
67 | Session.setUseWorker s (toJSBool False)
68 | Session.setMode s "ace/mode/haskell"
69 | r <- Editor.renderer e
70 | Renderer.setTheme r "ace/theme/tomorrow"
71 | Renderer.setShowGutter r (toJSBool False)
72 | Renderer.setShowPrintMargin r (toJSBool False)
73 | setGlobalEditor e
74 | return e
75 |
76 | -- Purely for development purposes
77 | foreign import javascript unsafe "window.ace_editor = $1;"
78 | setGlobalEditor :: Editor -> IO ()
79 |
80 | --------------------------------------------------------------------------------
81 | -- Queries
82 |
83 | getValue :: Editor -> IO Text
84 | getValue = fmap fromJSString . Editor.getValue
85 |
86 | getSelection :: Editor -> IO Selection
87 | getSelection editor = do
88 | s <- Editor.getSelection editor
89 | let fromRef = fromJSRefOrFail' "ace editor selection" . coerce
90 | lead <- Selection.getSelectionLead s
91 | anchor <- Selection.getSelectionAnchor s
92 | Selection <$> fromRef lead <*> fromRef anchor
93 |
94 | getCharPosition :: Editor -> Pos -> IO (Int, Int)
95 | getCharPosition editor pos = do
96 | renderer <- Editor.renderer editor
97 | obj <- Renderer.textToScreenCoordinates
98 | renderer
99 | (intToJSNumber (row pos))
100 | (intToJSNumber (column pos))
101 | (cx, cy) <- convertPagePosition obj
102 | (ex, ey) <-
103 | convertPagePosition =<<
104 | getPagePosition . coerce =<<
105 | Renderer.scroller renderer
106 | print (cx, cy, ex, ey)
107 | return (cx - ex, cy - ey)
108 |
109 | convertPagePosition :: JSRef obj -> IO (Int, Int)
110 | convertPagePosition obj =
111 | maybe (fail "Failed to convert page position") return =<<
112 | runMaybeT ((,) <$> mtprop obj "pageX" <*> mtprop obj "pageY")
113 |
114 | --FIXME: switch to using GHCJS.DOM.ClientRect once we update to a sufficient version
115 | --
116 | -- foreign import unsafe "$1.getBoundingClientRect()"
117 | -- getBoundingClientRect :: Element -> IO ClientRect
118 |
119 | foreign import javascript unsafe
120 | "function(el) {\
121 | var rect = el.getBoundingClientRect();\
122 | return { pageX: rect.left, pageY: rect.top };\
123 | }($1)"
124 | getPagePosition :: HTMLElement -> IO (JSRef obj)
125 |
126 | --------------------------------------------------------------------------------
127 | -- Mutations
128 |
129 | -- Note: -1 specifies that the cursor should be moved to the end
130 | --
131 | setValue :: Editor -> JSString -> IO ()
132 | setValue e val = void $ Editor.setValue e val (intToJSNumber (-1))
133 |
134 | setSelection :: Editor -> Selection -> IO ()
135 | setSelection editor Selection{..} = do
136 | s <- Editor.getSelection editor
137 | Selection.setSelectionAnchor s
138 | (intToJSNumber (row anchor))
139 | (intToJSNumber (column anchor))
140 | Selection.selectTo s
141 | (intToJSNumber (row lead))
142 | (intToJSNumber (column lead))
143 |
144 | newtype MarkerId = MarkerId Int
145 | deriving (Eq, Show)
146 |
147 | addMarker :: Editor -> Range -> JSString -> JSString -> Bool -> IO MarkerId
148 | addMarker editor range clazz typ inFront = do
149 | s <- Editor.session editor
150 | rangeRef <- toJSRef range
151 | inFrontRef <- toJSRef inFront
152 | MarkerId <$> js_addMarker s rangeRef clazz typ inFrontRef
153 |
154 | removeMarker :: Editor -> MarkerId -> IO ()
155 | removeMarker editor (MarkerId mid) = do
156 | s <- Editor.session editor
157 | js_removeMarker s mid
158 |
159 | --------------------------------------------------------------------------------
160 | -- Event Handlers
161 |
162 | onChange :: Editor -> (ChangeEvent -> IO ()) -> IO ()
163 | onChange e f = do
164 | -- TODO: open pull request on ace typescript bindings making things subtypes of EventEmitter?
165 | parent <- toJSRef =<< Editor.container e
166 | f' <- syncCallback1 (DomRetain (coerce parent)) True (f <=< fromJSRefOrFail)
167 | editorOn e "change" f'
168 |
169 | --TODO: would be cheaper to not marshal the lines and instead get
170 | --their length (at least for our purposes in this project).
171 |
172 | data ChangeEvent
173 | = InsertLines Range [JSString]
174 | | InsertText Range JSString
175 | | RemoveLines Range [JSString] Char
176 | | RemoveText Range JSString
177 | deriving (Typeable)
178 |
179 | instance FromJSRef ChangeEvent where
180 | fromJSRef obj = runMaybeT $ do
181 | inner <- mtprop obj "data"
182 | action <- mtprop inner "action"
183 | range <- mtprop inner "range"
184 | case action :: Text of
185 | "insertLines" -> InsertLines range <$> mtprop inner "lines"
186 | "insertText" -> InsertText range <$> mtprop inner "text"
187 | "removeLines" -> RemoveLines range <$> mtprop inner "lines"
188 | <*> mtprop inner "nl"
189 | "removeText" -> RemoveText range <$> mtprop inner "text"
190 | _ -> MaybeT (return Nothing)
191 |
192 | onSelectionChange :: Editor -> IO () -> IO ()
193 | onSelectionChange e f = do
194 | -- TODO: open pull request on ace typescript bindings making things subtypes of EventEmitter?
195 | parent <- toJSRef =<< Editor.container e
196 | f' <- syncCallback1 (DomRetain (coerce parent)) True (\_ -> f >> return jsNull)
197 | s <- Editor.getSelection e
198 | Selection.on s "changeCursor" f'
199 |
200 | addCommand :: Editor -> JSString -> JSString -> JSString -> IO () -> IO ()
201 | addCommand editor name win mac f = do
202 | cb <- asyncCallback AlwaysRetain f
203 | js_addCommand editor name win mac cb
204 |
205 | --------------------------------------------------------------------------------
206 | -- Positions and Ranges
207 | --
208 | -- Note: inclusion tests consider the range to be inclusive on the
209 | -- left (closed), and exclusive on the right (open).
210 |
211 | data Pos = Pos
212 | { row :: !Int
213 | , column :: !Int
214 | }
215 | deriving (Show, Eq, Ord, Generic, Typeable)
216 |
217 | data Range = Range
218 | { start :: !Pos
219 | , end :: !Pos
220 | }
221 | deriving (Show, Eq, Generic, Typeable)
222 |
223 | data Selection = Selection
224 | { anchor :: !Pos
225 | , lead :: !Pos
226 | }
227 | deriving (Show, Eq, Generic, Typeable)
228 |
229 | instance FromJSRef Pos where fromJSRef = fromJSRef_generic id
230 | instance FromJSRef Range where fromJSRef = fromJSRef_generic id
231 | instance FromJSRef Selection where fromJSRef = fromJSRef_generic id
232 |
233 | instance ToJSRef Range where
234 | toJSRef r = js_createRange
235 | (row (start r))
236 | (column (start r))
237 | (row (end r))
238 | (column (end r))
239 |
240 | selectionToRange :: Selection -> Range
241 | selectionToRange sel =
242 | if anchor sel < lead sel
243 | then Range (anchor sel) (lead sel)
244 | else Range (lead sel) (anchor sel)
245 |
246 | rangeToSelection :: Range -> Selection
247 | rangeToSelection Range {..} = Selection { anchor = start, lead = end }
248 |
249 | data RangeOrdering
250 | = Before
251 | | Intersecting
252 | | After
253 | deriving (Show, Eq, Ord, Generic, Typeable)
254 |
255 | compareRange :: Range -> Range -> RangeOrdering
256 | compareRange x y
257 | | end x < start y = Before
258 | | end y <= start x = After
259 | | otherwise = Intersecting
260 |
261 | comparePosWithRange :: Pos -> Range -> RangeOrdering
262 | comparePosWithRange pos range
263 | | pos < start range = Before
264 | | end range <= pos = Intersecting
265 | | otherwise = After
266 |
267 | data DeltaPos = DeltaPos
268 | { deltaRow :: !Int
269 | , deltaColumn :: !Int
270 | }
271 | deriving (Show, Eq, Ord, Generic, Typeable)
272 |
273 | subtractPos :: Pos -> Pos -> DeltaPos
274 | subtractPos x y = DeltaPos
275 | { deltaRow = row x - row y
276 | , deltaColumn = column x - column y
277 | }
278 |
279 | shiftPos :: DeltaPos -> Pos -> Pos
280 | shiftPos d p = Pos
281 | { row = deltaRow d + row p
282 | , column = deltaColumn d + column p
283 | }
284 |
285 | shiftRange :: DeltaPos -> Range -> Range
286 | shiftRange d r = Range
287 | { start = shiftPos d (start r)
288 | , end = shiftPos d (end r)
289 | }
290 |
291 | --------------------------------------------------------------------------------
292 | -- FFI
293 |
294 | -- Needed due to incompleteness ghcjs-from-typescript / the typescript parser
295 | foreign import javascript unsafe "function() { return ace; }()"
296 | ace :: IO Ace
297 |
298 | -- Needed due to incompleteness of typescript ace definitions
299 |
300 | foreign import javascript unsafe "$1.on($2, $3)"
301 | editorOn :: Editor -> JSString -> JSFun (JSRef obj -> IO ()) -> IO ()
302 |
303 | foreign import javascript unsafe "$1.setOption('maxLines', Infinity)"
304 | setMaxLinesInfty :: Editor -> IO ()
305 |
306 | foreign import javascript unsafe "$1.addMarker($2, $3, $4, $5)"
307 | js_addMarker :: IEditSession -> JSRef Range -> JSString -> JSString -> JSRef Bool -> IO Int
308 |
309 | foreign import javascript unsafe "$1.removeMarker($2)"
310 | js_removeMarker :: IEditSession -> Int -> IO ()
311 |
312 | foreign import javascript unsafe "function() { var Range = ace.require('ace/range').Range; return new Range($1,$2,$3,$4); }()"
313 | js_createRange :: Int -> Int -> Int -> Int -> IO (JSRef Range)
314 |
315 | foreign import javascript unsafe "$1.commands.addCommand({name:$2, bindKey:{win:$3, mac:$4, sender:'editor'}, exec:$5})"
316 | js_addCommand :: Editor -> JSString -> JSString -> JSString -> JSFun (IO ()) -> IO ()
317 |
--------------------------------------------------------------------------------
/soh-client/src/JavaScript/IFrame.hs:
--------------------------------------------------------------------------------
1 | module JavaScript.IFrame
2 | ( IFrame(..)
3 | , buildIFrame
4 | , setIFrameUrl
5 | ) where
6 |
7 | import Control.Lens (Traversal')
8 | import Control.Monad (void)
9 | import Control.Monad.IO.Class (MonadIO)
10 | import Data.Foldable (forM_)
11 | import Data.Text (Text)
12 | import Data.Typeable (Typeable)
13 | import GHCJS.DOM.Element (Element, elementSetAttribute)
14 | import GHCJS.DOM.Node (nodeAppendChild)
15 | import GHCJS.Types (JSString)
16 | import Import.Util (getElement)
17 | import React.Internal (Component, ReactT)
18 | import JavaScript.Unmanaged (Unmanaged, buildUnmanaged)
19 | import GHCJS.Marshal (ToJSRef, FromJSRef)
20 | import Prelude
21 |
22 | newtype IFrame = IFrame Element
23 | deriving (Typeable, ToJSRef, FromJSRef)
24 |
25 | buildIFrame
26 | :: MonadIO m
27 | => Component state (Unmanaged IFrame) m
28 | -> Traversal' state (Unmanaged IFrame)
29 | -> Maybe Text
30 | -> ReactT state m ()
31 | buildIFrame c l murl = buildUnmanaged c l $ \_ q -> do
32 | parent <- getElement 0 q
33 | el <- createIFrame
34 | forM_ murl $ elementSetAttribute el ("src" :: JSString)
35 | void $ nodeAppendChild parent (Just el)
36 | return (IFrame el)
37 |
38 | setIFrameUrl :: IFrame -> Text -> IO ()
39 | setIFrameUrl (IFrame el) = elementSetAttribute el ("src" :: JSString)
40 |
41 | foreign import javascript unsafe "document.createElement('iframe')"
42 | createIFrame :: IO Element
43 |
--------------------------------------------------------------------------------
/soh-client/src/JavaScript/TermJs.hs:
--------------------------------------------------------------------------------
1 | -- | Bindings to the TermJs terminal emulator.
2 | module JavaScript.TermJs
3 | ( TermJs(..)
4 | , initTerminal
5 | , writeTerminal
6 | , onTerminalData
7 | , onTerminalDisconnect
8 | ) where
9 |
10 | import Data.Coerce (coerce)
11 | import Data.Text (Text)
12 | import Data.Typeable (Typeable)
13 | import GHCJS.DOM.HTMLElement (HTMLElement)
14 | import GHCJS.Foreign
15 | import GHCJS.Marshal
16 | import GHCJS.Types
17 | import Import.Util (getElement)
18 | import JavaScript.JQuery (JQuery)
19 | import Prelude
20 |
21 | newtype TermJs = TermJs (JSRef TermJs)
22 | deriving (ToJSRef, FromJSRef, Typeable)
23 |
24 | initTerminal :: JQuery -> IO TermJs
25 | initTerminal q = do
26 | terminal <- newTerminal
27 | openTerminal terminal =<< getElement 0 q
28 | return terminal
29 |
30 | writeTerminal :: TermJs -> Text -> IO ()
31 | writeTerminal terminal = writeTerminal' terminal . toJSString
32 |
33 | onTerminalData :: TermJs -> (Text -> IO ()) -> IO ()
34 | onTerminalData terminal f = do
35 | parent <- toJSRef =<< terminalParent terminal
36 | f' <- syncCallback1 (DomRetain (coerce parent)) True (f . fromJSString)
37 | onTerminalData' terminal f'
38 |
39 | onTerminalDisconnect :: TermJs -> IO () -> IO ()
40 | onTerminalDisconnect terminal f = do
41 | parent <- toJSRef =<< terminalParent terminal
42 | f' <- syncCallback (DomRetain (coerce parent)) True f
43 | onTerminalDisconnect' terminal f'
44 |
45 | --------------------------------------------------------------------------------
46 | -- FFI
47 |
48 | foreign import javascript
49 | "new Terminal({ useStyle: true, screenKeys: true })"
50 | newTerminal :: IO TermJs
51 |
52 | foreign import javascript
53 | "$1.open($2)"
54 | openTerminal :: TermJs -> HTMLElement -> IO ()
55 |
56 | -- foreign import javascript
57 | -- "$1.destroy()"
58 | -- destroyTerminal :: TermJs -> IO ()
59 |
60 | -- foreign import javascript
61 | -- "$1.focus()"
62 | -- focusTerminal :: TermJs -> IO ()
63 |
64 | foreign import javascript
65 | "$1.write($2)"
66 | writeTerminal' :: TermJs -> JSString -> IO ()
67 |
68 | foreign import javascript
69 | "$1.on('data', $2)"
70 | onTerminalData' :: TermJs -> JSFun (JSString -> IO ()) -> IO ()
71 |
72 | foreign import javascript
73 | "$1.on('disconnect', $2)"
74 | onTerminalDisconnect' :: TermJs -> JSFun (IO ()) -> IO ()
75 |
76 | foreign import javascript
77 | "$1.parent"
78 | terminalParent :: TermJs -> IO HTMLElement
79 |
--------------------------------------------------------------------------------
/soh-client/src/JavaScript/Unmanaged.hs:
--------------------------------------------------------------------------------
1 | -- | A React component which allows direct DOM construction and has a
2 | -- single "init" property which specifies a callback for when the
3 | -- component is mounted. Particularly useful for using widgets
4 | -- written in javascript which do not have react bindings, or when
5 | -- react's model does not match well with the domain.
6 | module JavaScript.Unmanaged
7 | ( Unmanaged(..)
8 | , getDefUnmanaged
9 | , newUnmanaged
10 | , buildUnmanaged
11 | , onInitUnmanaged
12 | , getUnmanaged
13 | , getUnmanagedOrFail
14 | , readUnmanaged
15 | , readUnmanagedOrFail
16 | ) where
17 |
18 | import Control.Concurrent.STM
19 | import Control.Lens
20 | import Control.Monad.IO.Class (MonadIO)
21 | import Control.Monad.Reader (ask)
22 | import Data.Proxy (Proxy(Proxy))
23 | import Data.Typeable (Typeable, typeRep)
24 | import GHCJS.Foreign
25 | import GHCJS.Marshal
26 | import GHCJS.Types
27 | import Import.Util (expectProp)
28 | import JavaScript.JQuery (JQuery)
29 | import Prelude
30 | import React hiding (onClick)
31 | import React.Builder (refAttr)
32 | import React.Internal
33 |
34 | newtype Unmanaged a = Unmanaged (JSRef a)
35 |
36 | instance Show (Unmanaged a) where
37 | show _ = "<