├── .github
└── workflows
│ └── nodejs.yml
├── .gitignore
├── .npmignore
├── .tidyrc.json
├── LICENSE
├── README.md
├── index.js
├── package-lock.json
├── package.json
├── packages.dhall
├── spago.dhall
└── src
├── Main.js
├── Main.purs
└── Pscid
├── Console.js
├── Console.purs
├── Error.purs
├── Keypress.js
├── Keypress.purs
├── Options.js
├── Options.purs
├── Process.purs
├── Psa.purs
├── Server.purs
└── Util.purs
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | strategy:
11 | matrix:
12 | node-version: [16.x, 18.x, 20.x]
13 |
14 | steps:
15 | - uses: actions/checkout@v3
16 | - name: Use Node.js ${{ matrix.node-version }}
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: ${{ matrix.node-version }}
20 | - name: npm install, build, and test
21 | run: |
22 | npm ci
23 | npm run build
24 | npm run format:check
25 | env:
26 | CI: true
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /output/
3 | /.psci*
4 | /.psa-stash
5 | .DS_Store
6 | /.psc-ide-port
7 | /.spago*
8 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /.github/
2 | /.spago/
3 | /.psci*
4 | /output/*/externs.cbor
5 | /output/cache-db.json
6 | *.dhall
7 | /.psc-ide-port
8 |
--------------------------------------------------------------------------------
/.tidyrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "importSort": "ide",
3 | "importWrap": "source",
4 | "indent": 2,
5 | "operatorsFile": null,
6 | "ribbon": 1,
7 | "typeArrowPlacement": "first",
8 | "unicode": "never",
9 | "width": null
10 | }
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/kritzcreek/pscid/actions)
2 | [](https://badge.fury.io/js/pscid)
3 |
4 | pscid
5 | ===
6 |
7 | An editor agnostic minimal IDE for your shell. Think `spago -w build` on steroids.
8 |
9 | ### Installation
10 |
11 | `npm i -g pscid`
12 |
13 | ### Usage
14 |
15 | Start `pscid` in a terminal in the root folder of your project.
16 |
17 | pscid will show you errors and warnings (one at a time) whenever you save a PureScript source file. This makes for a nice iterative workflow.
18 |
19 | Type `b` inside `pscid`'s terminal window to build your project. This looks up the `pscid:build` script inside your package.json, then falls back to the `build` script, and then finally tries `spago build`.
20 |
21 | Type `t` inside `pscid`'s terminal window to test your project. As with building this looks up the `pscid:test` script first, then `test`, then falls back to `spago test` as a last resort.
22 |
23 | Type `q` to quit pscid.
24 |
25 | ### Suggestions
26 |
27 | Some warnings carry a suggestion from the compiler (for example redundant
28 | imports). `pscid` will prompt you to press `s` inside the terminal window when
29 | it encounters such a warning, and automatically apply the suggestion for you.
30 |
31 | ### Demo
32 |
33 | 
34 |
35 | ### Options
36 | - `-p` The port to use. Defaults to 4243
37 | - `--include -I
` Additional directories for PureScript source files, separated by `;`
38 | - `--censor-codes ` Warning codes to ignore, seperated by `,` (just like in purescript-psa)
39 | - `--test` Runs your tests after every successful rebuild
40 | - `-O/--output` Specifies what output directory to use to load the externs for the server
41 |
42 | ### Attribution
43 |
44 | pscid utilizes https://github.com/natefaubion/purescript-psa to format and enrich the errors and warnings emitted by the compiler.
45 |
46 | It's inspired by https://github.com/ndmitchell/ghcid and https://github.com/anttih/psc-pane.
47 |
48 | ### LICENSE
49 |
50 | Copyright 2023 Christoph Hegemann and Contributors
51 |
52 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
53 |
54 | See the LICENSE file for further details.
55 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { main } from './output/Main/index.js'
3 | main()
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pscid",
3 | "type": "module",
4 | "version": "2.11.0",
5 | "description": "A lightweight editor experience for PureScript development",
6 | "repository": "https://github.com/kritzcreek/pscid",
7 | "bin": {
8 | "pscid": "index.js"
9 | },
10 | "directories": {
11 | "test": "test"
12 | },
13 | "scripts": {
14 | "compile": "spago build",
15 | "prepack": "rimraf output && rimraf dist && npm run compile",
16 | "build": "npm run -s compile",
17 | "start": "node index.js",
18 | "format": "purs-tidy format-in-place 'src/**/*.purs'",
19 | "format:check": "purs-tidy check 'src/**/*.purs'"
20 | },
21 | "keywords": [
22 | "IDE",
23 | "purescript"
24 | ],
25 | "author": "kritzcreek",
26 | "license": "LGPL-3.0",
27 | "dependencies": {
28 | "gaze": "^1.1.3",
29 | "glob": "^10.3.10",
30 | "keypress": "^0.2.1",
31 | "which": "^4.0.0"
32 | },
33 | "devDependencies": {
34 | "purescript": "^0.15.10",
35 | "purs-tidy": "^0.10.0",
36 | "rimraf": "^5.0.5",
37 | "spago": "^0.21.0"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/packages.dhall:
--------------------------------------------------------------------------------
1 | let upstream =
2 | https://github.com/purescript/package-sets/releases/download/psc-0.15.8-20230528/packages.dhall sha256:7f1ebc6968ebabae915640456faf98f52f5606189f48cb22305fbe008723f26c
3 |
4 | in upstream
5 | with suggest =
6 | { dependencies =
7 | [ "argonaut-codecs"
8 | , "argonaut-core"
9 | , "arrays"
10 | , "bifunctors"
11 | , "console"
12 | , "effect"
13 | , "either"
14 | , "foldable-traversable"
15 | , "lists"
16 | , "maybe"
17 | , "node-buffer"
18 | , "node-fs"
19 | , "node-process"
20 | , "node-streams"
21 | , "ordered-collections"
22 | , "prelude"
23 | , "psa-utils"
24 | , "refs"
25 | , "strings"
26 | ]
27 | , repo = "https://github.com/nwolverson/purescript-suggest.git"
28 | , version = "c866dd7408902313c45bb579715f479f7f268162"
29 | }
30 |
--------------------------------------------------------------------------------
/spago.dhall:
--------------------------------------------------------------------------------
1 | {-
2 | Welcome to a Spago project!
3 | You can edit this file as you like.
4 | -}
5 | { name = "pscid"
6 | , dependencies =
7 | [ "aff"
8 | , "ansi"
9 | , "argonaut"
10 | , "arrays"
11 | , "bifunctors"
12 | , "console"
13 | , "control"
14 | , "effect"
15 | , "either"
16 | , "exceptions"
17 | , "foldable-traversable"
18 | , "maybe"
19 | , "node-buffer"
20 | , "node-child-process"
21 | , "node-fs"
22 | , "node-process"
23 | , "node-streams"
24 | , "optparse"
25 | , "ordered-collections"
26 | , "partial"
27 | , "prelude"
28 | , "psa-utils"
29 | , "psc-ide"
30 | , "refs"
31 | , "strings"
32 | , "suggest"
33 | , "transformers"
34 | , "typelevel-prelude"
35 | ]
36 | , packages = ./packages.dhall
37 | , sources = [ "src/**/*.purs" ]
38 | }
39 |
--------------------------------------------------------------------------------
/src/Main.js:
--------------------------------------------------------------------------------
1 | //module Main
2 |
3 | import gaze_ from 'gaze';
4 |
5 | export function gaze(globs, cb) {
6 | gaze_(globs, { follow: true }, function (err, watcher) {
7 | // Files have all started watching
8 | // watcher === this
9 |
10 | // Get all watched files
11 | var watched = watcher.watched();
12 |
13 | watcher.on('changed', function (filepath) {
14 | cb(filepath)();
15 | });
16 |
17 | watcher.on('added', function (filepath) {
18 | cb(filepath)();
19 | });
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/src/Main.purs:
--------------------------------------------------------------------------------
1 | module Main where
2 |
3 | import Prelude
4 |
5 | import Control.Monad.Reader (class MonadAsk, ReaderT, ask, runReaderT)
6 | import Data.Argonaut (Json)
7 | import Data.Array as Array
8 | import Data.Either (Either(..), either, isRight)
9 | import Data.Maybe (Maybe(..))
10 | import Data.String (Pattern(..))
11 | import Data.String as String
12 | import Effect (Effect)
13 | import Effect.Aff (Milliseconds(..), attempt, delay, launchAff_, runAff)
14 | import Effect.Class (class MonadEffect, liftEffect)
15 | import Effect.Class.Console as Console
16 | import Effect.Ref (Ref)
17 | import Effect.Ref as Ref
18 | import Effect.Uncurried (EffectFn2, runEffectFn2)
19 | import Node.Process as Process
20 | import Psa (PsaError)
21 | import PscIde (cwd, load, sendCommandR)
22 | import PscIde.Command (Command(..), Message(..))
23 | import Pscid.Console (clearConsole, owl, startScreen, suggestionHint)
24 | import Pscid.Error (catchLog, noSourceDirectoryError)
25 | import Pscid.Keypress (Key(..), initializeKeypresses, onKeypress)
26 | import Pscid.Options (PscidSettings, optionParser, printCLICommand)
27 | import Pscid.Process (execCommand)
28 | import Pscid.Psa (filterWarnings, parseErrors, psaPrinter)
29 | import Pscid.Server (restartServer, startServer', stopServer')
30 | import Pscid.Util (both)
31 | import Suggest (applySuggestions)
32 | import Type.Prelude as TE
33 |
34 | newtype Pscid a = Pscid (ReaderT (PscidSettings Int) Effect a)
35 |
36 | derive newtype instance functorPscid :: Functor Pscid
37 | derive newtype instance applyPscid :: Apply Pscid
38 | derive newtype instance applicativePscid :: Applicative Pscid
39 | derive newtype instance bindPscid :: Bind Pscid
40 | derive newtype instance monadPscid :: Monad Pscid
41 | instance monadAskPscid :: TE.TypeEquals r (PscidSettings Int) => MonadAsk r Pscid where
42 | ask = Pscid (map TE.from ask)
43 |
44 | instance monadEffectPscid :: MonadEffect Pscid where
45 | liftEffect = Pscid <<< liftEffect
46 |
47 | runPscid :: forall a. Pscid a -> PscidSettings Int -> Effect a
48 | runPscid (Pscid f) e = runReaderT f e
49 |
50 | newtype State = State { errors :: Array PsaError }
51 |
52 | emptyState :: State
53 | emptyState = State { errors: [] }
54 |
55 | main :: Effect Unit
56 | main = launchAff_ do
57 | config@{ port, outputDirectory, sourceDirectories } <- liftEffect optionParser
58 | when (Array.null sourceDirectories) (liftEffect noSourceDirectoryError)
59 | stateRef <- liftEffect (Ref.new emptyState)
60 | Console.log "Starting purs ide server"
61 | r <- attempt (startServer' port outputDirectory)
62 | case r of
63 | Right (Right port') -> do
64 | let config' = config { port = port' }
65 | Message directory <- do
66 | delay (Milliseconds 500.0)
67 | _ <- load port' [] []
68 | res <- cwd port'
69 | case res of
70 | Right d -> pure d
71 | Left err -> liftEffect do
72 | Console.log err
73 | Process.exit 1
74 | liftEffect do
75 | runEffectFn2 gaze
76 | (Array.concatMap fileGlob sourceDirectories)
77 | (\d -> runPscid (triggerRebuild stateRef d) config')
78 | clearConsole
79 | initializeKeypresses
80 | onKeypress (\k -> runPscid (keyHandler stateRef k) config')
81 | Console.log ("Watching " <> directory <> " on port " <> show port')
82 | startScreen
83 | Right (Left errMsg) ->
84 | Console.log ("Failed to start psc-ide-server with: " <> errMsg)
85 | Left err ->
86 | Console.log ("Failed to start psc-ide-server with : " <> show err)
87 |
88 | -- | Given a directory, appends the globs necessary to match all PureScript and
89 | -- | JavaScript source files inside that directory
90 | fileGlob :: String -> Array String
91 | fileGlob dir =
92 | let
93 | go x = dir <> "/**/*" <> x
94 | in
95 | go <$> [ ".purs", ".js" ]
96 |
97 | keyHandler :: Ref State -> Key -> Pscid Unit
98 | keyHandler stateRef k = do
99 | { port, buildCommand, outputDirectory, testCommand } <- ask
100 | case k of
101 | Key { ctrl: false, name: "b", meta: false } ->
102 | liftEffect (execCommand "Build" $ printCLICommand buildCommand)
103 | Key { ctrl: false, name: "t", meta: false } ->
104 | liftEffect (execCommand "Test" $ printCLICommand testCommand)
105 | Key { ctrl: false, name: "r", meta: false } -> liftEffect do
106 | clearConsole
107 | catchLog "Failed to restart server" $ launchAff_ do
108 | restartServer port outputDirectory
109 | _ <- load port [] []
110 | pure unit
111 | Console.log owl
112 | Key { ctrl: false, name: "s", meta: false } -> liftEffect do
113 | State state <- Ref.read stateRef
114 | case Array.head state.errors of
115 | Nothing ->
116 | Console.log "No suggestions available"
117 | Just e ->
118 | catchLog "Couldn't apply suggestion." (applySuggestions [ e ])
119 | Key { ctrl: false, name: "q", meta: false } ->
120 | liftEffect (Console.log "Bye!" <* runAff (either exit exit) (stopServer' port))
121 | Key { ctrl: true, name: "c", meta: false } ->
122 | Console.log "Press q to exit"
123 | Key { name } ->
124 | Console.log name
125 | where
126 | exit :: forall a. a -> Effect Unit
127 | exit = const (Process.exit 0)
128 |
129 | triggerRebuild :: Ref State -> String -> Pscid Unit
130 | triggerRebuild stateRef file = do
131 | { port, testCommand, testAfterRebuild, censorCodes } <- ask
132 | let fileName = changeExtension file "purs"
133 | liftEffect <<< catchLog "We couldn't talk to the server" $ launchAff_ do
134 | result <- sendCommandR port (RebuildCmd fileName Nothing Nothing)
135 | case result of
136 | Left _ -> Console.log "We couldn't talk to the server"
137 | Right errs -> liftEffect do
138 | parsedErrors <- handleRebuildResult fileName censorCodes errs
139 | Ref.write (State { errors: parsedErrors }) stateRef
140 | case Array.head parsedErrors >>= _.suggestion of
141 | Nothing -> pure unit
142 | Just _ -> suggestionHint
143 | when (testAfterRebuild && isRight errs)
144 | (execCommand "Test" $ printCLICommand testCommand)
145 |
146 | changeExtension :: String -> String -> String
147 | changeExtension s ex =
148 | case String.lastIndexOf (Pattern ".") s of
149 | Nothing ->
150 | s
151 | Just ix ->
152 | String.take ix s <> "." <> ex
153 |
154 | handleRebuildResult
155 | :: String
156 | -> Array String
157 | -> Either Json Json
158 | -> Effect (Array PsaError)
159 | handleRebuildResult file censorCodes result = do
160 | clearConsole
161 | Console.log ("Checking " <> file)
162 | case both parseErrors result of
163 | Right warnings ->
164 | either
165 | (\_ -> Console.log "Failed to parse warnings" $> [])
166 | (\e -> psaPrinter owl false e $> e)
167 | (filterWarnings censorCodes <$> warnings)
168 | Left errors ->
169 | either
170 | (\_ -> Console.log "Failed to parse errors" $> [])
171 | (\e -> psaPrinter owl true e $> e)
172 | errors
173 |
174 | foreign import gaze :: EffectFn2 (Array String) (String -> Effect Unit) Unit
175 |
--------------------------------------------------------------------------------
/src/Pscid/Console.js:
--------------------------------------------------------------------------------
1 | export function clearConsole() {
2 | process.stdout.write('\x1Bc');
3 | }
4 |
--------------------------------------------------------------------------------
/src/Pscid/Console.purs:
--------------------------------------------------------------------------------
1 | module Pscid.Console where
2 |
3 | import Prelude
4 |
5 | import Ansi.Codes (Color(..))
6 | import Ansi.Output (foreground, withGraphics)
7 | import Effect (Effect)
8 | import Effect.Console as Console
9 |
10 | logColored :: Color -> String -> Effect Unit
11 | logColored c s = Console.log (withGraphics (foreground c) s)
12 |
13 | owl :: String
14 | owl =
15 | """
16 | ___ ,_, ___ ,_, ___
17 | (o,o) (o,o) ,,,(o,o),,, (o,o) (o,o)
18 | {`"'} {`"'} ';:`-':;' {`"'} {`"'}
19 | -"-"- -"-"- -"-"- -"-"-
20 | """
21 |
22 | helpText :: String
23 | helpText =
24 | """
25 | Press b to run a full build (tries "npm run pscid:build" then "npm run build" then "spago build")
26 | Press t to test (tries "npm run pscid:test" then "npm run test" then "spago test")
27 | Press r to reset
28 | Press q to quit
29 | """
30 |
31 | startScreen :: Effect Unit
32 | startScreen = do
33 | Console.log owl
34 | logColored Blue helpText
35 |
36 | suggestionHint :: Effect Unit
37 | suggestionHint =
38 | logColored Blue "Press s to automatically apply the suggestion."
39 |
40 | foreign import clearConsole :: Effect Unit
41 |
--------------------------------------------------------------------------------
/src/Pscid/Error.purs:
--------------------------------------------------------------------------------
1 | module Pscid.Error where
2 |
3 | import Prelude
4 |
5 | import Ansi.Codes (Color(..))
6 | import Effect (Effect)
7 | import Effect.Console as Console
8 | import Effect.Exception (catchException)
9 | import Node.Process as Process
10 | import Pscid.Console (logColored)
11 |
12 | catchLog :: String -> Effect Unit -> Effect Unit
13 | catchLog m = catchException (const (Console.error m))
14 |
15 | noSourceDirectoryError :: Effect Unit
16 | noSourceDirectoryError = do
17 | logColored Red "ERROR:"
18 | Console.log helpString
19 | Process.exit 1
20 | where
21 | helpString =
22 | """I couldn't find any source directories to watch when trying app/, src/, test/ and tests/.
23 | You can specify your own semicolon separated list of folders to check with the -I option like so:
24 |
25 | $ pscid -I "sources;tests" """
26 |
--------------------------------------------------------------------------------
/src/Pscid/Keypress.js:
--------------------------------------------------------------------------------
1 | // module Pscid.Keypress
2 |
3 | import keypress from 'keypress';
4 |
5 | export function initializeKeypresses() {
6 | keypress(process.stdin);
7 | process.stdin.setRawMode(true);
8 | process.stdin.resume();
9 | }
10 |
11 | export function onKeypress(cb) {
12 | return function () {
13 | process.stdin.on('keypress', function (ch, key) {
14 | if (key) {
15 | cb(key)();
16 | }
17 | });
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/src/Pscid/Keypress.purs:
--------------------------------------------------------------------------------
1 | module Pscid.Keypress where
2 |
3 | import Prelude
4 |
5 | import Effect (Effect)
6 |
7 | newtype Key = Key
8 | { ctrl :: Boolean
9 | , name :: String
10 | , meta :: Boolean
11 | , shift :: Boolean
12 | }
13 |
14 | foreign import initializeKeypresses :: Effect Unit
15 | foreign import onKeypress :: (Key -> Effect Unit) -> Effect Unit
16 |
--------------------------------------------------------------------------------
/src/Pscid/Options.js:
--------------------------------------------------------------------------------
1 | //module Pscid.Options
2 |
3 | import { createRequire } from 'module';
4 | const require = createRequire(import.meta.url);
5 |
6 | export function hasNamedScript(name) {
7 | return function () {
8 | try {
9 | var pjson = require(process.cwd() + '/package.json');
10 | return pjson.scripts && pjson.scripts[name];
11 | } catch (e) {
12 | return false;
13 | }
14 | };
15 | }
16 |
17 | export function glob(pattern) {
18 | return function () {
19 | return require('glob').sync(pattern);
20 | };
21 | }
22 |
23 | export function version() {
24 | // This one references pscid's package.json
25 | var pjson = require('../../package.json');
26 | if (!pjson) {
27 | return 'Unknown';
28 | } else {
29 | return pjson.version;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Pscid/Options.purs:
--------------------------------------------------------------------------------
1 | module Pscid.Options where
2 |
3 | import Prelude
4 |
5 | import Control.Alt ((<|>))
6 | import Control.MonadPlus (guard)
7 | import Data.Array as Array
8 | import Data.Maybe (Maybe(..), fromMaybe, optional)
9 | import Data.String as String
10 | import Effect (Effect)
11 | import Effect.Console as Console
12 | import Node.Platform (Platform(..))
13 | import Node.Process (platform)
14 | import Node.Process as Process
15 | import Options.Applicative as OA
16 |
17 | type PscidSettings a =
18 | { port :: a
19 | , buildCommand :: CLICommand
20 | , outputDirectory :: String
21 | , testCommand :: CLICommand
22 | , testAfterRebuild :: Boolean
23 | , sourceDirectories :: Array String
24 | , censorCodes :: Array String
25 | }
26 |
27 | type PscidOptions = PscidSettings (Maybe Int)
28 |
29 | defaultOptions :: PscidOptions
30 | defaultOptions =
31 | { port: Nothing
32 | , buildCommand: BuildCommand (spagoCmd <> " build") []
33 | , outputDirectory: "output"
34 | , testCommand: BuildCommand (spagoCmd <> " test") []
35 | , testAfterRebuild: false
36 | , sourceDirectories: []
37 | , censorCodes: []
38 | }
39 |
40 | -- | Scans the default directories and returns those, that did contain
41 | -- | PureScript files.
42 | scanDefaultDirectories :: Effect (Array String)
43 | scanDefaultDirectories =
44 | let
45 | defaultDirectories = [ "src", "app", "test", "tests" ]
46 | mkGlob dir = dir <> "/**/*.purs"
47 | in
48 | Array.filterA (map (not <<< Array.null) <<< glob <<< mkGlob) defaultDirectories
49 |
50 | spagoCmd :: String
51 | spagoCmd = if platform == Just Win32 then "spago.cmd" else "spago"
52 |
53 | npmCmd :: String
54 | npmCmd = if platform == Just Win32 then "npm.cmd" else "npm"
55 |
56 | mkDefaultOptions :: Effect PscidOptions
57 | mkDefaultOptions =
58 | (defaultOptions { buildCommand = _, testCommand = _, sourceDirectories = _ })
59 | <$> mkCommand "build"
60 | <*> mkCommand "test"
61 | <*> scanDefaultDirectories
62 |
63 | type IncludePath = String
64 |
65 | data CLICommand
66 | = ScriptCommand String
67 | | BuildCommand String (Array IncludePath)
68 |
69 | printCLICommand :: CLICommand -> String
70 | printCLICommand = case _ of
71 | ScriptCommand str ->
72 | str
73 | BuildCommand str [] ->
74 | str
75 | BuildCommand str includes ->
76 | str <> " -I " <> String.joinWith ":" includes
77 |
78 | -- | If the command is a BuildCommand (eg. "spago build"), then the array
79 | -- | of include paths is set. If the command is an NPM script, the command is
80 | -- | left unchanged. This is because it's impossible to guarantee that the NPM
81 | -- | script directly executes "spago build" (it may execute another
82 | -- | script), and therefore we cannot simply append the includes onto the end of
83 | -- | the command.
84 | setCommandIncludes :: Array IncludePath -> CLICommand -> CLICommand
85 | setCommandIncludes includesArr cmd = case cmd of
86 | BuildCommand str _ -> BuildCommand str includesArr
87 | ScriptCommand str -> ScriptCommand str
88 |
89 | mkCommand :: String -> Effect CLICommand
90 | mkCommand cmd = do
91 | pscidSpecific <- hasNamedScript ("pscid:" <> cmd)
92 | namedScript <- hasNamedScript cmd
93 |
94 | let
95 | npmSpecificCommand =
96 | guard pscidSpecific $> ScriptCommand (npmCmd <> " run -s pscid:" <> cmd)
97 |
98 | npmBuildCommand =
99 | guard namedScript $> ScriptCommand (npmCmd <> " run -s " <> cmd)
100 |
101 | buildCommand = BuildCommand (spagoCmd <> " " <> cmd) []
102 |
103 | pure $ fromMaybe buildCommand (npmSpecificCommand <|> npmBuildCommand)
104 |
105 | -- | Accepts defaults options and
106 | buildOptions
107 | :: PscidOptions
108 | -> { port :: Maybe Int
109 | , testAfterRebuild :: Boolean
110 | , includes :: String
111 | , outputDirectory :: String
112 | , censor :: String
113 | }
114 | -> PscidOptions
115 | buildOptions defaults { port, testAfterRebuild, includes, outputDirectory, censor } = do
116 | { port
117 | , testAfterRebuild
118 | , sourceDirectories: defaults.sourceDirectories <> includesArr
119 | , censorCodes: sepArguments "," censor
120 | , outputDirectory
121 | , buildCommand: setCommandIncludes includesArr defaults.buildCommand
122 | , testCommand: setCommandIncludes includesArr defaults.testCommand
123 | }
124 | where
125 | includesArr = sepArguments ";" includes
126 |
127 | sepArguments :: String -> String -> Array String
128 | sepArguments sep =
129 | Array.filter (not String.null) <<< String.split (String.Pattern sep)
130 |
131 | -- | A parser for pscid's options. A `Nothing` signals the user
132 | -- | requested the version
133 | options :: PscidOptions -> OA.Parser (Maybe PscidOptions)
134 | options defaults = ado
135 | displayVersion <- OA.switch
136 | ( OA.long "version"
137 | <> OA.help "Displays the version of this program"
138 | )
139 | port <- optional
140 | ( OA.option
141 | OA.int
142 | ( OA.long "port"
143 | <> OA.short 'p'
144 | <> OA.metavar "PORT"
145 | <> OA.help "What port to start the ide server on"
146 | )
147 | )
148 | testAfterRebuild <- OA.switch
149 | ( OA.long "test"
150 | <> OA.help "Run tests after successful rebuild"
151 | )
152 | includes <-
153 | OA.strOption
154 | ( OA.long "include"
155 | <> OA.short 'I'
156 | <> OA.help "Directories for additional PureScript source files, separated by `;`"
157 | <> OA.value ""
158 | <> OA.metavar "INCLUDES"
159 | )
160 | <|> pure ""
161 | censor <-
162 | OA.strOption
163 | ( OA.long "censor-codes"
164 | <> OA.help "Warning codes to ignore, seperated by `,`"
165 | <> OA.value ""
166 | <> OA.metavar "CENSOR-CODES"
167 | )
168 | <|> pure ""
169 | outputDirectory <-
170 | OA.strOption
171 | ( OA.long "output"
172 | <> OA.short 'O'
173 | <> OA.help "Output directory for compiled JavaScript"
174 | <> OA.value "output"
175 | <> OA.metavar "OUTPUT"
176 | )
177 | <|> pure "output"
178 | in
179 | if displayVersion then Nothing
180 | else Just (buildOptions defaults { port, testAfterRebuild, outputDirectory, includes, censor })
181 |
182 | optionParser :: Effect PscidOptions
183 | optionParser = do
184 | defaults <- mkDefaultOptions
185 | OA.execParser (opts defaults) >>= case _ of
186 | Nothing -> do
187 | Console.log =<< version
188 | Process.exit 0
189 | Just os ->
190 | pure os
191 | where
192 | opts defaults = OA.info (options defaults OA.<**> OA.helper)
193 | ( OA.fullDesc
194 | <> OA.progDesc "Watches and rebuilds PureScript source files"
195 | <> OA.header "pscid - A lightweight, fast and unintrusive PureScript file-watcher"
196 | )
197 |
198 | foreign import hasNamedScript :: String -> Effect Boolean
199 | foreign import glob :: String -> Effect (Array String)
200 | foreign import version :: Effect String
201 |
--------------------------------------------------------------------------------
/src/Pscid/Process.purs:
--------------------------------------------------------------------------------
1 | module Pscid.Process where
2 |
3 | import Prelude
4 |
5 | import Ansi.Codes (Color(..))
6 | import Data.Array as Array
7 | import Data.Maybe (fromJust)
8 | import Data.String as String
9 | import Effect (Effect)
10 | import Effect.Console as Console
11 | import Effect.Ref as Ref
12 | import Node.ChildProcess (Exit(..), defaultSpawnOptions, onExit, spawn, stderr, stdout)
13 | import Node.Encoding (Encoding(..))
14 | import Node.Stream (onDataString)
15 | import Partial.Unsafe (unsafePartial)
16 | import Pscid.Console (logColored)
17 | import Pscid.Error (catchLog)
18 |
19 | execCommand :: String -> String -> Effect Unit
20 | execCommand name command = catchLog (name <> " threw an exception") do
21 | let cmd = unsafePartial fromJust (Array.uncons (String.split (String.Pattern " ") command))
22 | output <- Ref.new ""
23 | Console.log ("Running: \"" <> command <> "\"")
24 | cp <- spawn cmd.head cmd.tail defaultSpawnOptions
25 |
26 | let
27 | stout = stdout cp
28 | sterr = stderr cp
29 |
30 | onDataString stout UTF8 \s ->
31 | Ref.modify_ (_ <> s) output
32 |
33 | onDataString sterr UTF8 \s ->
34 | Ref.modify_ (_ <> s) output
35 |
36 | onExit cp \e -> case e of
37 | Normally 0 -> logColored Green (name <> " successful!")
38 | Normally code -> do
39 | Console.log =<< Ref.read output
40 | logColored Red (name <> " errored with code: " <> show code)
41 | BySignal _ -> pure unit
42 |
--------------------------------------------------------------------------------
/src/Pscid/Psa.purs:
--------------------------------------------------------------------------------
1 | module Pscid.Psa
2 | ( module Psa
3 | , parseErrors
4 | , psaPrinter
5 | , filterWarnings
6 | ) where
7 |
8 | import Prelude
9 |
10 | import Data.Argonaut (Json, decodeJson, printJsonDecodeError)
11 | import Data.Array as Array
12 | import Data.Bifunctor (lmap)
13 | import Data.Either (Either)
14 | import Data.Maybe (Maybe(..), fromMaybe)
15 | import Data.Maybe as Maybe
16 | import Data.Set as Set
17 | import Data.String as String
18 | import Data.Traversable (traverse)
19 | import Effect (Effect)
20 | import Effect.Console as Console
21 | import Effect.Exception (catchException)
22 | import Node.Encoding as Encoding
23 | import Node.FS.Sync as File
24 | import Psa (Output, PsaError, PsaOptions, PsaResult, StatVerbosity(..), Suggestion, output, parsePsaError)
25 | import Psa.Printer (renderAnsi, renderRow)
26 | import Psa.Printer.Default (renderError, renderWarning)
27 | import Psa.Util (iter_)
28 |
29 | defaultOptions :: PsaOptions
30 | defaultOptions =
31 | { ansi: true
32 | , censorWarnings: false
33 | , censorLib: false
34 | , censorSrc: false
35 | , censorCodes: Set.empty
36 | , filterCodes: Set.empty
37 | , libDirs: []
38 | , strict: false
39 | , cwd: ""
40 | , statVerbosity: NoStats
41 | }
42 |
43 | print :: String -> PsaOptions -> Output -> Effect Unit
44 | print successMessage options { warnings, errors } = do
45 | iter_ warnings \_ warning -> do
46 | Console.error (toString (renderWarning 1 1 warning))
47 | Console.error ""
48 |
49 | iter_ errors \_ error -> do
50 | Console.error (toString (renderError 1 1 error))
51 | Console.error ""
52 |
53 | when (Array.null warnings && Array.null errors)
54 | (Console.error successMessage)
55 | where
56 | toString = renderRow (String.joinWith "" <<< map (renderAnsi options.ansi))
57 |
58 | parseErrors :: Json -> Either String (Array PsaError)
59 | parseErrors j = traverse parsePsaError =<< (lmap printJsonDecodeError $ decodeJson j)
60 |
61 | emptyResult :: PsaResult
62 | emptyResult = { warnings: [], errors: [] }
63 |
64 | wrapError :: Boolean -> PsaError -> PsaResult
65 | wrapError b e =
66 | if b then { warnings: [], errors: [ e ] }
67 | else { warnings: [ e ], errors: [] }
68 |
69 | psaPrinter :: String -> Boolean -> Array PsaError -> Effect Unit
70 | psaPrinter successMessage isError errs =
71 | catchException (const (Console.error "An error inside psaPrinter")) do
72 | out' <- output loadLines defaultOptions result
73 | print successMessage defaultOptions out'
74 | where
75 | result = fromMaybe emptyResult (wrapError isError <$> Array.head errs)
76 |
77 | loadLines
78 | :: forall a
79 | . String
80 | -> { startLine :: Int, endLine :: Int | a }
81 | -> Effect (Maybe (Array String))
82 | loadLines filename pos = do
83 | contents <- String.split (String.Pattern "\n") <$> File.readTextFile Encoding.UTF8 filename
84 | let source = Array.slice (pos.startLine - 1) (pos.endLine) contents
85 | pure (Just source)
86 |
87 | -- | We do this to push imports suggestions for the Prelude to the
88 | -- | back of the suggestions, so all other modules can be explicitly
89 | -- | imported first through the suggestions
90 | reorderWarnings :: Array PsaError -> Array PsaError
91 | reorderWarnings errors =
92 | let
93 | isPreludeImport :: Suggestion -> Boolean
94 | isPreludeImport { replacement } =
95 | -- We're checking for two patterns to also catch the case of
96 | -- custom preludes that still have Prelude in their name
97 | ( String.contains (String.Pattern "import")
98 | && String.contains (String.Pattern "Prelude")
99 | ) replacement
100 | { yes, no } =
101 | Array.partition (Maybe.maybe false isPreludeImport <<< _.suggestion) errors
102 | in
103 | no <> yes
104 |
105 | -- | Removes warnings that have their error codes ignored and moves
106 | -- | import suggestions for the Prelude to the back
107 | filterWarnings :: Array String -> Array PsaError -> Array PsaError
108 | filterWarnings ignored errors =
109 | reorderWarnings (Array.filter (\e -> e.errorCode `Array.notElem` ignored) errors)
110 |
--------------------------------------------------------------------------------
/src/Pscid/Server.purs:
--------------------------------------------------------------------------------
1 | module Pscid.Server
2 | ( restartServer
3 | , startServer'
4 | , stopServer'
5 | , module PscIde.Server
6 | ) where
7 |
8 | import Prelude
9 |
10 | import Control.Alt ((<|>))
11 | import Data.Either (Either(..))
12 | import Data.Maybe (Maybe(..), maybe)
13 | import Effect.Aff (Aff, attempt)
14 | import Effect.Class (liftEffect)
15 | import Effect.Console as Console
16 | import Effect.Exception (try)
17 | import Node.ChildProcess as CP
18 | import Node.Process as Process
19 | import PscIde as PscIde
20 | import PscIde.Command (Message(..))
21 | import PscIde.Server (ServerStartResult(..), defaultServerArgs, deleteSavedPort, getSavedPort, pickFreshPort, savePort, startServer, stopServer)
22 |
23 | stopServer' :: Int -> Aff Unit
24 | stopServer' port = do
25 | _ <- liftEffect (Process.cwd >>= try <<< deleteSavedPort)
26 | stopServer port
27 |
28 | startServer'
29 | :: Maybe Int
30 | -> String
31 | -> Aff (Either String Int)
32 | startServer' optPort outputDir = do
33 | dir <- liftEffect Process.cwd
34 | port <- liftEffect (getSavedPort dir)
35 | case optPort <|> port of
36 | Just p -> do
37 | workingDir <- attempt (PscIde.cwd p)
38 | case workingDir of
39 | -- If we find an already running server with the right working
40 | -- directory, we just return its port.
41 | Right (Right (Message dir')) | dir == dir' -> pure (Right p)
42 | -- Otherwise we start a new server
43 | _ -> launchServer dir
44 | Nothing -> launchServer dir
45 |
46 | where
47 | launchServer :: String -> Aff (Either String Int)
48 | launchServer dir = do
49 | newPort <- maybe (liftEffect pickFreshPort) pure optPort
50 | _ <- liftEffect (try (savePort newPort dir))
51 | r newPort <$>
52 | startServer
53 | ( defaultServerArgs
54 | { port = Just newPort
55 | , cwd = Just dir
56 | , outputDirectory = Just outputDir
57 | , stdio = CP.ignore
58 | }
59 | )
60 | where
61 | r newPort (Started _) = Right newPort
62 | r _ (Closed) = Left "Closed"
63 | r _ (StartError s) = Left s
64 |
65 | restartServer :: Int -> String -> Aff Unit
66 | restartServer port outputDir = do
67 | _ <- attempt (stopServer port)
68 | r <- attempt (startServer' (Just port) outputDir)
69 | liftEffect case r of
70 | Left e -> do
71 | Console.log
72 | ( "Failed to restart psc-ide-server on port: " <> show port
73 | <> "\nThe error was: "
74 | <> show e
75 | )
76 | Process.exit 1
77 | Right _ -> Console.log "I restarted psc-ide-server for you."
78 |
--------------------------------------------------------------------------------
/src/Pscid/Util.purs:
--------------------------------------------------------------------------------
1 | module Pscid.Util (both) where
2 |
3 | import Data.Either (Either(..))
4 |
5 | both :: forall a b. (a -> b) -> Either a a -> Either b b
6 | both f e = case e of
7 | Right x -> Right (f x)
8 | Left x -> Left (f x)
9 |
--------------------------------------------------------------------------------