├── .ghci ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .hlint.yaml ├── .vscode └── launch.json ├── .weeder.yaml ├── CHANGES.txt ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── Setup.hs ├── ghcid.cabal ├── plugins ├── README.md ├── emacs │ ├── README.md │ └── ghcid.el ├── nvim │ ├── LICENSE │ ├── README.md │ ├── doc │ │ └── neovim-ghcid.txt │ └── ftplugin │ │ └── haskell.vim └── vscode │ ├── .gitignore │ ├── .vscode │ ├── launch.json │ ├── settings.json │ └── tasks.json │ ├── .vscodeignore │ ├── README.md │ ├── package.json │ ├── src │ └── extension.ts │ ├── test │ ├── extension.test.ts │ └── index.ts │ ├── tsconfig.json │ ├── vsc-extension-quickstart.md │ └── yarn.lock ├── src ├── Ghcid.hs ├── Language │ └── Haskell │ │ ├── Ghcid.hs │ │ └── Ghcid │ │ ├── Escape.hs │ │ ├── Parser.hs │ │ ├── Terminal.hs │ │ ├── Types.hs │ │ └── Util.hs ├── Paths.hs ├── Session.hs ├── Test.hs ├── Test │ ├── API.hs │ ├── Common.hs │ ├── Ghcid.hs │ ├── Parser.hs │ └── Util.hs └── Wait.hs └── test ├── bar ├── bar.cabal └── src │ ├── Boot.hs │ ├── Boot.hs-boot │ ├── Haskell.hs │ └── Literate.lhs ├── foo ├── .ghci ├── Paths.hs ├── Root.hs └── Test.hs └── project-stack ├── project-x.cabal └── src └── ProjectX.hs /.ghci: -------------------------------------------------------------------------------- 1 | :set -Wunused-binds -Wunused-imports -Worphans 2 | :set -isrc 3 | :load Ghcid src/Paths.hs Test 4 | :def docs_ const $ return $ unlines [":!cabal haddock"] 5 | :def docs const $ return $ unlines [":docs_",":!start dist\\doc\\html\\ghcid\\Language-Haskell-Ghcid.html"] 6 | :def test const $ return $ unlines ["Test.main"] 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '0 3 * * 6' # 3am Saturday 7 | jobs: 8 | test: 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [ubuntu-latest] 15 | ghc: ['9.6', '9.4', '9.2', '9.0', '8.10', '8.8'] 16 | include: 17 | - os: windows-latest 18 | - os: macOS-latest 19 | 20 | steps: 21 | - run: git config --global core.autocrlf false 22 | - uses: actions/checkout@v2 23 | - uses: haskell/actions/setup@v2 24 | id: setup-haskell 25 | with: 26 | ghc-version: ${{ matrix.ghc }} 27 | - run: cabal v2-freeze --enable-tests 28 | - uses: actions/cache@v2 29 | with: 30 | path: ${{ steps.setup-haskell.outputs.cabal-store }} 31 | key: ${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }} 32 | restore-keys: ${{ runner.os }}-${{ matrix.ghc }}- 33 | - uses: ndmitchell/neil@master 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /.dist-buildwrapper 3 | /.project 4 | /cabal.sandbox.config 5 | tags 6 | /.cabal-sandbox 7 | .stack-work 8 | *.vsix 9 | # Use 'stack init' on a fresh checkout to generate your own stack.yaml file 10 | # for easy building. 11 | /stack.yaml 12 | /issues 13 | 14 | dist-newstyle 15 | -------------------------------------------------------------------------------- /.hlint.yaml: -------------------------------------------------------------------------------- 1 | # HLint configuration file 2 | # https://github.com/ndmitchell/hlint 3 | ########################## 4 | 5 | - ignore: {name: Use camelCase, within: Ghcid} # For cmdargs argument names 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${file}", 12 | "outFiles": [ 13 | "${workspaceFolder}/**/*.js" 14 | ] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /.weeder.yaml: -------------------------------------------------------------------------------- 1 | # Weeder configuration file 2 | # https://github.com/ndmitchell/weeder 3 | ########################## 4 | 5 | - message: Module reused between components 6 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | Changelog for ghcid (* = breaking change) 2 | 3 | 0.8.9, released 2023-07-02 4 | #375, fix crash when modules are renamed or deleted in GHC 9.6 5 | #378, write output of the linter into output files 6 | Support newer Win32 libraries 7 | 0.8.8, released 2022-09-17 8 | #365, support fsnotify 0.4 9 | #348, improve the no files loaded message 10 | #321, print Ghcide has stopped to the output file. 11 | 0.8.7, released 2020-06-06 12 | #319, allow eval multiline commands 13 | 0.8.6, released 2020-04-13 14 | #314, report GHC panics as error messages 15 | 0.8.5, released 2020-03-19 16 | #311, allow --target with Stack 17 | 0.8.4, released 2020-03-15 18 | #309, use cabal repl --repl-options to avoid errors 19 | 0.8.3, released 2020-03-10 20 | #306, fix --target to work on exe: targets 21 | 0.8.2, released 2020-03-08 22 | #302, --target option for specifying targets for cabal repl 23 | #300, restart on file changes detected by --restart=dir/ 24 | 0.8.1, released 2020-01-09 25 | #293, passing --allow-eval should disable -fno-code by default 26 | #291, try and be more robust to mucking with the console 27 | 0.8, released 2019-12-04 28 | * Add an extra field to GhciError 29 | #288, include the last line of stderr in shutdown messages 30 | 0.7.7, released 2019-11-24 31 | #287, only lint reloaded files 32 | 0.7.6, released 2019-10-13 33 | #283, support Clashi 34 | #278, allow --lint to take a shell command rather than just an executable 35 | #275, fix error reporting with non .hs/.lhs filenames in LINE pragmas 36 | 0.7.5, released 2019-07-04 37 | #263, fix not resizing on terminal resize 38 | #262, add --test-message flag 39 | #253, try and keep the cursor visible after Ctrl-C 40 | #248, add --allow-eval flag to evaluate code snippets 41 | #245, link with -rtsopts to be able to use RTS flags 42 | 0.7.4, released 2019-04-17 43 | #237, ability to cope better with large error messages 44 | #237, add --clear, --no-height-limit, --reverse-errors 45 | 0.7.3, released 2019-04-15 46 | #236, add hlint support, pass --lint 47 | 0.7.2, released 2019-03-12 48 | #225, allow watching directories for new files 49 | #226, fix the polling behaviour 50 | #224, print out overly long files without line breaks 51 | #202, make --verbose print out version, arch, os 52 | 0.7.1, released 2018-09-07 53 | #190, load benchmarks as well as tests when using stack 54 | #171, support GHCJSi 55 | #163, allow --run, like --test but defaults to main 56 | #161, if a file disappears, reload as soon as it returns 57 | #162, always disable :set +t (which writes out types) 58 | 0.7, released 2018-04-18 59 | #153, show errors/warnings to freshly edited files first 60 | #153, set -j 61 | #154, improve Ctrl-C behaviour and process termination during startup 62 | Require extra-1.6.6 63 | Add --setup actions to send to ghci stdin on startup 64 | #152, make --output for a file ending .json produce JSON output 65 | Make module cycles generate well-formed Load messages 66 | * Add loadFilePosEnd to Load 67 | If a command exits unexpectedly return a failing exit code 68 | #120, use absolute paths everywhere, in case ghci does a cd 69 | Add showPaths function to exercise ":show paths" 70 | #127, use Loaded GHCi configuration message to add .ghci restart 71 | #141, restart if .ghcid changes 72 | #151, make "All good" green 73 | #109, support OverloadedStrings and RebindableSyntax together 74 | Remove support for GHC 7.6 75 | #87, enable HSPEC colors if possible (POSIX only) 76 | #147, move some :set flags to the command line if possible 77 | #148, enable -ferror-spans by default 78 | * #144, allow ANSI escape codes in loadMessage 79 | #144, use color GHC messages where possible 80 | #139, add --color=never to not show Bold output 81 | #142, add startGhciProcess which takes CreateProcess 82 | #146, add --max-messages=N to limit to N messages 83 | 0.6.10, released 2018-02-06 84 | #96, make it work even if -fdiagnostics-color is active 85 | #130, pass -fno-it to reduce memory leaks 86 | #132, work even if -fhide-source-paths is set 87 | Add isLoading function to test the constructor 88 | 0.6.9, released 2018-01-26 89 | #121, add a --poll flag to use polling instead of notifiers 90 | Require time-1.5 or above 91 | #124, add timestamp to "All good" message 92 | After the test completes write a message 93 | 0.6.8, released 2017-11-10 94 | #110, work even if -v0 is passed to ghci 95 | Read additional arguments from .ghcid file if present 96 | 0.6.7, released 2017-09-17 97 | #104, add --ignore-loaded flag for use with -fobject-code 98 | #103, deal with new GHC error formatting 99 | 0.6.6, released 2016-11-11 100 | #89, exit sooner when the child process exits unexpectedly 101 | Add Eq instance for Ghci 102 | #88, add a --project flag 103 | Rename --notitle flag to --no-title 104 | 0.6.5, released 2016-08-10 105 | #82, properly deal with warning messages including spans 106 | #78, support boot files better 107 | Better support for dealing with GHC 8.0 call stack messages 108 | #79, support multiple --test flags 109 | #71, improve multi-project stack support 110 | #74, make sure Cygwin terminals flush output properly 111 | If the test exits then exit ghcid 112 | 0.6.4, released 2016-05-13 113 | #69, fix up for stack project with file arguments 114 | 0.6.3, released 2016-05-11 115 | #68, add --no-status to avoid printing the reloading message 116 | 0.6.2, released 2016-04-25 117 | #63, detect test failures and update the titlebar/icon 118 | #66, add --warnings to run tests even in there are warnings 119 | #62, find stack.yaml in the parent directory 120 | Make --verbose echo all things sent to stdin 121 | #57, support wrappers that access stdin first (e.g. stack) 122 | #67, make --reload/--restart recurse through directories 123 | #61, deal with drive letters in files (for stack on Windows) 124 | #58, improve the --help message 125 | 0.6.1, released 2016-04-06 126 | Add --reload to add files that reload, but do not restart 127 | #56, allow --restart to take directories 128 | 0.6, released 2016-04-06 129 | #38, implement loading with stack 130 | Add process, quit and execStream to the API 131 | #29, add interrupt function 132 | Add Data instances for the types 133 | Make stopGhci more effective, now kills the underlying process 134 | * Make startGhci take a function to write the buffer to 135 | 0.5.1, released 2016-03-02 136 | #17, deal with recursive modules errors properly 137 | #50, use -fno-code when not running tests (about twice as fast) 138 | #44, abbreviate the redundant module import error 139 | #45, add an extra space before the ... message 140 | #42, always show the first error in full 141 | #43, work even if people use break-on-exception flags 142 | #42, make the first error a minimum of 5 lines 143 | 0.5, released 2015-06-20 144 | * Add an extra boolean argument to startGhci 145 | Add the number of modules loaded after All good 146 | Print out messages until the prompt comes up 147 | #23, add arguments and change what commands get invoked 148 | #35, change the titlebar icon on Windows 149 | 0.4.2, released 2015-06-11 150 | Fix a GHC 7.6 warning 151 | 0.4.1, released 2015-06-11 152 | #37, add a --notitle flag 153 | Require extra-1.2 154 | 0.4, released 2015-06-07 155 | #33, make Ctrl-C more robust 156 | #31, add an outputfile feature 157 | #32, make newer warnings first (save a file, view its warnings) 158 | #28, fix issues on VIM file saves 159 | #29, support running a quick test on each save 160 | Add a --directory flag to change directory first 161 | #26, use fs-notify to avoid excessive wakeups 162 | #25, detect console size just before using it 163 | 0.3.6, released 2015-03-09 164 | #24, don't error out if error/putStrLn are not imported 165 | 0.3.5, released 2015-02-25 166 | #19, put errors in bold 167 | #9, display interesting information in the title bar 168 | #7, reload if the .ghci or .cabal file changes 169 | Use nubOrd 170 | Require extra-1.1 171 | 0.3.4, released 2014-12-24 172 | #21, if you aren't waiting for any files, exit 173 | 0.3.3, released 2014-12-21 174 | #21, if the root file is missing, report an error 175 | #20, avoid an O(n^2) nub 176 | 0.3.2, released 2014-11-06 177 | #18, reformat excessively long lines, add a --width flag 178 | 0.3.1, released 2014-10-28 179 | Ensure if there are lots of warnings, the first error gets shown 180 | 0.3, released 2014-10-24 181 | #11, ignore certain GHCi-only warnings 182 | #13, fix version printing 183 | #8, display Loading... when starting 184 | Require the extra library 185 | #14, figure out the terminal height automatically 186 | 0.2, released 2014-10-06 187 | #6, rewrite as a library 188 | Remove duplicate error messages from cabal repl 189 | 0.1.3, released 2014-09-29 190 | #2, handle files that get deleted while loaded 191 | #3, flesh out the test suite 192 | #4, give a polite error if ghci does not start 193 | #5, add --topmost flag to make the window topmost 194 | Add a very simple test suite 195 | Default to cabal repl if there is no .ghci file 196 | #1, if there is an IOError just :reload 197 | Say why you are reloading 198 | 0.1.1, released 2014-09-27 199 | Support arguments to --command 200 | 0.1, released 2014-09-27 201 | Initial version 202 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Neil Mitchell 2014-2023. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Neil Mitchell nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thanks for the pull request! 2 | 3 | By raising this pull request you confirm you are licensing your contribution under all licenses that apply to this project (see LICENSE) and that you have no patents covering your contribution. 4 | 5 | If you care, my PR preferences are at https://github.com/ndmitchell/neil#contributions, but they're all guidelines, and I'm not too fussy - you don't have to read them. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ghcid [![Hackage version](https://img.shields.io/hackage/v/ghcid.svg?label=Hackage)](https://hackage.haskell.org/package/ghcid) [![Stackage version](https://www.stackage.org/package/ghcid/badge/nightly?label=Stackage)](https://www.stackage.org/package/ghcid) [![Build status](https://img.shields.io/github/actions/workflow/status/ndmitchell/ghcid/ci.yml?branch=master)](https://github.com/ndmitchell/ghcid/actions) 2 | 3 | Either "GHCi as a daemon" or "GHC + a bit of an IDE". To a first approximation, it opens `ghci` and runs `:reload` whenever your source code changes, formatting the output to fit a fixed height console. Unlike other Haskell development tools, `ghcid` is intended to be _incredibly simple_. In particular, it doesn't integrate with any editors, doesn't provide access to the `ghci` it starts, doesn't depend on GHC the library and doesn't start web servers. 4 | 5 | _Acknowledgements:_ This project incorporates significant work from [JPMoresmau](https://github.com/JPMoresmau), who is listed as a co-author. 6 | 7 | ### Using it 8 | 9 | Run `stack install ghcid` or `cabal update && cabal install ghcid` to install it as normal. Then run `ghcid "--command=ghci Main.hs"`. The `command` is how you start your project in `ghci`. If you omit `--command` then it will default to `stack ghci` if you have the `stack.yaml` file and `.stack-work` directory, default to `ghci` if you have a `.ghci` file in the current directory, and otherwise default to `cabal repl`. 10 | 11 | Personally, I always create a `.ghci` file at the root of all my projects, which usually [reads something like](https://github.com/ndmitchell/ghcid/blob/master/.ghci): 12 | 13 | :set -fwarn-unused-binds -fwarn-unused-imports 14 | :set -isrc 15 | :load Main 16 | 17 | After that, resize your console and make it so you can see it while working in your editor. On Windows you may wish to pass `--topmost` so the console will sit on top of all other windows. On Linux, you probably want to use your window manager to make it topmost or use a [tiling window manager](http://xmonad.org/). 18 | 19 | ### What you get 20 | 21 | On every save you'll see a list of the errors and warnings in your project. It uses `ghci` under the hood, so even relatively large projects should update their status pretty quickly. As an example: 22 | 23 | Main.hs:23:10: 24 | Not in scope: `verbosit' 25 | Perhaps you meant `verbosity' (imported from System.Console.CmdArgs) 26 | Util.hs:18:1: Warning: Defined but not used: `foo' 27 | 28 | Or, if everything is good, you see: 29 | 30 | All good 31 | 32 | Please [report any bugs](https://github.com/ndmitchell/ghcid/issues) you find. 33 | 34 | ### Editor integration 35 | 36 | There are a few plugins that integrate Ghcid into editors, notably: 37 | 38 | * [VS Code](plugins/vscode/) 39 | * [nvim](plugins/nvim/) 40 | * [vim](https://github.com/aiya000/vim-ghcid-quickfix) 41 | * [Emacs](plugins/emacs/) 42 | 43 | ### Usage tips 44 | 45 | * If you have a `.ghcid` file in the current folder, or a parent folder, the contents of that file will be used as command line arguments. For example, if you always want to pass `--command=custom-ghci` then putting that in a `.ghcid` file will free you from writing it each time. 46 | * There is an article on [auto-reloading threepenny-gui apps during development](https://binarin.ru/post/auto-reload-threepenny-gui/). 47 | * There are a list of [general tips for using Ghcid](http://www.parsonsmatt.org/2018/05/19/ghcid_for_the_win.html). 48 | 49 | In general, to use `ghcid`, you first need to get `ghci` working well for you. In particular, craft a command line or `.ghci` file such that when you start `ghci` it has loaded all the files you care about (check `:show modules`). If you want to use `--test` check that whatever expression you want to use works in that `ghci` session. Getting `ghci` started properly is one of the hardest things of using `ghcid`, and while `ghcid` has a lot of defaults for common cases, it doesn't always work out of the box. 50 | 51 | ### Evaluation 52 | 53 | Using the `ghci` session that `ghcid` manages you can also evaluate expressions: 54 | 55 | * You can pass any `ghci` expression with the `--test` flag, e.g. `--test=:main`, which will be run whenever the code is warning free (or pass `--warnings` for when the code is merely error free). 56 | * If you pass the `--allow-eval` flag then comments in the source files such as `-- $> expr` will run `expr` after loading - see [this blog post](https://jkeuhlen.com/2019/10/19/Compile-Your-Comments-In-Ghcid.html) for more details. Multiline comments are also supported with the following syntax: 57 | 58 | {- $> 59 | expr1 60 | expr2 61 | ... 62 | exprN 63 | <$ -} 64 | 65 | Expressions that read from standard input are likely to hang, given that Ghcid already uses the standard input to interact with Ghci. 66 | 67 | ### FAQ 68 | 69 | #### This isn't as good as full IDE 70 | I've gone for simplicity over features. It's a point in the design space, but not necessarily the best point in the design space for you. Other points in the design space include: 71 | 72 | * [ghcide](https://github.com/digital-asset/ghcide) - a real IDE in your editor. 73 | * [reflex-ghci](https://github.com/reflex-frp/reflex-ghci) - like Ghcid but with more terminal UI features. 74 | * [reflex-ghcide](https://github.com/mpickering/reflex-ghcide) - a full IDE in the terminal. 75 | 76 | #### If I delete a file and put it back it gets stuck. 77 | Yes, that's a [bug in GHCi](https://ghc.haskell.org/trac/ghc/ticket/9648). If you see GHCi getting confused just kill `ghcid` and start it again. 78 | 79 | #### I want to run arbitrary commands when arbitrary files change. 80 | This project reloads `ghci` when files loaded by `ghci` change. If you want a more general mechanism, consider: 81 | 82 | * [Steel Overseer](https://github.com/schell/steeloverseer) ([Hackage](https://hackage.haskell.org/package/steeloverseer)) 83 | * [Watchman](https://facebook.github.io/watchman/) 84 | * [`feedback`](https://github.com/NorfairKing/feedback) ([Hackage](https://hackage.haskell.org/package/feedback)) 85 | * [Watchexec](https://github.com/watchexec/watchexec) 86 | * [`entr`](https://github.com/eradman/entr) 87 | 88 | #### I want syntax highlighting in the error messages. 89 | One option is to use Neovim or Emacs and run the terminal in a buffer whose file type is set to Haskell. Another option is to pipe `ghcid` through [source-highlight](https://www.gnu.org/software/src-highlite/) (`ghcid | source-highlight -s haskell -f esc`). 90 | 91 | #### I'm not seeing pattern matching warnings. 92 | Ghcid automatically appends `-fno-code` to the command line, which makes the reload cycle about twice as fast. Unfortunately GHC 8.0 and 8.2 suffer from [bug 10600](https://ghc.haskell.org/trac/ghc/ticket/10600) which means `-fno-code` also disables pattern matching warnings. On these versions, either accept no pattern match warnings or use `-c` to specify a command line to start `ghci` that doesn't include `-fno-code`. From GHC 8.4 this problem no longer exists. 93 | 94 | #### I get "During interactive linking, GHCi couldn't find the following symbol" 95 | This problem is a manifestation of [GHC bug 8025](https://ghc.haskell.org/trac/ghc/ticket/8025), which is fixed in GHC 8.4 and above. Ghcid automatically appends `-fno-code` to the command line, but for older GHC's you can supress that with `--test "return ()"` (to add a fake test) or `-c "ghci ..."` to manually specify the command to run. 96 | 97 | #### I only see source-spans or colors on errors/warnings after the first load. 98 | Due to limitations in `ghci`, these flags are only set _after_ the first load. If you want them to apply from the start, pass them on the command line to `ghci` with something like `-c "ghci -ferror-spans -fdiagnostics-color=always"`. 99 | 100 | #### I want to match on the file/line/column to get jump-to-error functionality in my editor. 101 | You will variously see `file:line:col:message`, `file:line:col1-col2:msg` and `file:(line1,col1)-(line2,col2):message`, as these are the formats GHC uses. To match all of them you can use a regular expression such as `^(\\S*?):(?|(\\d+):(\\d+)(?:-\\d+)?|\\((\\d+),(\\d+)\\)-\\(\\d+,\\d+\\)):([^\n]*)`. 102 | 103 | #### What if the error message is too big for my console? 104 | You can let `ghcid` print more with `--no-height-limit`. The first error message might end up outside of the console view, so you can use `--reverse-errors` to flip the order of the errors and warnings. Further error messages are just a scroll away. Finally if you're going to be scrolling, you can achieve a cleaner experience with the `--clear` flag, which clears the console on reload. 105 | 106 | #### I use Alex (`.x`) and Happy (`.y`) files, how can I check them? 107 | 108 | Ghcid only notices when the `.hs` files change. To make it respond to other files you can pass the `.x` and `.y` files to `--restart`, e.g. `--restart=myparser.y`. As long as you set the initial command to something that runs Happy/Alex (e.g. `cabal repl`) then when those files change everything will restart, causing the initial command to be rerun. 109 | 110 | #### How do I run pass command arguments with --test? 111 | 112 | `ghcid ... --test Main.main --setup ":set args myargs"` 113 | 114 | #### Why do I get "addWatch: resource exhausted (No space left on device)" or "openFile: resource exhausted (Too many open files)" on my Mac? 115 | 116 | The Mac has a fairly low limit on the number of file handles available. You can increase it with: `sudo sysctl -w fs.inotify.max_user_watches=262144; sudo sysctl -p` 117 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /ghcid.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 1.18 2 | build-type: Simple 3 | name: ghcid 4 | version: 0.8.9 5 | license: BSD3 6 | license-file: LICENSE 7 | category: Development 8 | author: Neil Mitchell , jpmoresmau 9 | maintainer: Neil Mitchell 10 | copyright: Neil Mitchell 2014-2023 11 | synopsis: GHCi based bare bones IDE 12 | description: 13 | Either \"GHCi as a daemon\" or \"GHC + a bit of an IDE\". A very simple Haskell development tool which shows you the errors in your project and updates them whenever you save. Run @ghcid --topmost --command=ghci@, where @--topmost@ makes the window on top of all others (Windows only) and @--command@ is the command to start GHCi on your project (defaults to @ghci@ if you have a @.ghci@ file, or else to @cabal repl@). 14 | homepage: https://github.com/ndmitchell/ghcid#readme 15 | bug-reports: https://github.com/ndmitchell/ghcid/issues 16 | tested-with: GHC==9.6, GHC==9.4, GHC==9.2, GHC==9.0, GHC==8.10, GHC==8.8 17 | extra-doc-files: 18 | CHANGES.txt 19 | README.md 20 | 21 | source-repository head 22 | type: git 23 | location: https://github.com/ndmitchell/ghcid.git 24 | 25 | library 26 | hs-source-dirs: src 27 | default-language: Haskell2010 28 | build-depends: 29 | base >= 4.7 && < 5, 30 | filepath, 31 | time >= 1.5, 32 | directory >= 1.2, 33 | extra >= 1.6.20, 34 | process >= 1.1, 35 | ansi-terminal, 36 | cmdargs >= 0.10 37 | 38 | exposed-modules: 39 | Language.Haskell.Ghcid 40 | other-modules: 41 | Paths_ghcid 42 | Language.Haskell.Ghcid.Escape 43 | Language.Haskell.Ghcid.Parser 44 | Language.Haskell.Ghcid.Types 45 | Language.Haskell.Ghcid.Util 46 | 47 | executable ghcid 48 | hs-source-dirs: src 49 | default-language: Haskell2010 50 | ghc-options: -main-is Ghcid.main -threaded -rtsopts 51 | main-is: Ghcid.hs 52 | build-depends: 53 | base >= 4.7 && < 5, 54 | filepath, 55 | time >= 1.5, 56 | directory >= 1.2, 57 | containers, 58 | fsnotify >= 0.4, 59 | extra >= 1.6.20, 60 | process >= 1.1, 61 | cmdargs >= 0.10, 62 | ansi-terminal, 63 | terminal-size >= 0.3 64 | if os(windows) 65 | build-depends: Win32 >= 2.13.2.1 66 | else 67 | build-depends: unix 68 | other-modules: 69 | Language.Haskell.Ghcid.Escape 70 | Language.Haskell.Ghcid.Parser 71 | Language.Haskell.Ghcid.Terminal 72 | Language.Haskell.Ghcid.Types 73 | Language.Haskell.Ghcid.Util 74 | Language.Haskell.Ghcid 75 | Paths_ghcid 76 | Session 77 | Wait 78 | 79 | test-suite ghcid_test 80 | type: exitcode-stdio-1.0 81 | hs-source-dirs: src 82 | main-is: Test.hs 83 | ghc-options: -rtsopts -main-is Test.main -threaded -with-rtsopts=-K1K 84 | default-language: Haskell2010 85 | build-depends: 86 | base >= 4.7 && < 5, 87 | filepath, 88 | time >= 1.5, 89 | directory >= 1.2, 90 | process, 91 | containers, 92 | fsnotify >= 0.4, 93 | extra >= 1.6.6, 94 | ansi-terminal, 95 | terminal-size >= 0.3, 96 | cmdargs, 97 | tasty, 98 | tasty-hunit 99 | if os(windows) 100 | build-depends: Win32 101 | else 102 | build-depends: unix 103 | other-modules: 104 | Ghcid 105 | Language.Haskell.Ghcid 106 | Language.Haskell.Ghcid.Escape 107 | Language.Haskell.Ghcid.Parser 108 | Language.Haskell.Ghcid.Terminal 109 | Language.Haskell.Ghcid.Types 110 | Language.Haskell.Ghcid.Util 111 | Paths_ghcid 112 | Session 113 | Test.API 114 | Test.Common 115 | Test.Ghcid 116 | Test.Parser 117 | Test.Util 118 | Wait 119 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # Ghcid editor support 2 | 3 | See the individual plugin directories for instructions on how to install. 4 | 5 | ### Contributing 6 | 7 | When contributing to plugin code, please prefix all commits with the editor name. Example: 8 | 9 | [nvim] Update error-matching code 10 | -------------------------------------------------------------------------------- /plugins/emacs/README.md: -------------------------------------------------------------------------------- 1 | Here's super basic emacs support for ghcid. This uses a terminal with compilation-mode. 2 | 3 | This code also assumes that stack is installed, which maybe isn't a great assumption. I had to do that in the past to get ghcid to behave properly with my stack environment, but perhaps ghcid works well now with stack without having to invoke the stack command directly. 4 | 5 | Originally written by @WraithM. 6 | -------------------------------------------------------------------------------- /plugins/emacs/ghcid.el: -------------------------------------------------------------------------------- 1 | ;;; ghcid.el --- Really basic ghcid+stack support in emacs with compilation-mode -*- lexical-binding: t -*- 2 | 3 | ;; Author: Matthew Wraith 4 | ;; Yorick Sijsling 5 | ;; Maintainer: Matthew Wraith 6 | ;; Yorick Sijsling 7 | ;; Vasiliy Yorkin 8 | ;; Neil Mitchell 9 | ;; URL: https://github.com/ndmitchell/ghcid 10 | ;; Version: 1.0 11 | ;; Created: 26 Sep 2014 12 | ;; Keywords: tools, files, Haskell 13 | 14 | ;;; Commentary: 15 | 16 | ;; Use M-x ghcid to launch 17 | 18 | ;;; Code: 19 | (require 'compile) 20 | (require 'term) 21 | 22 | ;; Set ghcid-target to change the stack target 23 | (setq ghcid-target "") 24 | 25 | (setq ghcid-process-name "ghcid") 26 | 27 | 28 | (define-minor-mode ghcid-mode 29 | "A minor mode for ghcid terminals 30 | 31 | Use `ghcid' to start a ghcid session in a new buffer. The process 32 | will start in the directory of your current buffer. 33 | 34 | It is based on `compilation-mode'. That means the errors and 35 | warnings can be clicked and the `next-error'(\\[next-error]) and 36 | `previous-error'(\\[previous-error]) commands will work as usual. 37 | 38 | To configure where the new buffer should appear, customize your 39 | `display-buffer-alist'. For instance like so: 40 | 41 | (add-to-list 42 | \\='display-buffer-alist 43 | \\='(\"*ghcid*\" 44 | (display-buffer-reuse-window ;; First try to reuse an existing window 45 | display-buffer-at-bottom ;; Then try a new window at the bottom 46 | display-buffer-pop-up-window) ;; Otherwise show a pop-up 47 | (window-height . 18) ;; New window will be 18 lines 48 | )) 49 | 50 | If the window that shows ghcid changes size, the process will not 51 | recognize the new height until you manually restart it by calling 52 | `ghcid' again. 53 | " 54 | :lighter " Ghcid" 55 | (when (fboundp 'nlinum-mode) (nlinum-mode -1)) 56 | (linum-mode -1) 57 | (compilation-minor-mode)) 58 | 59 | 60 | ;; Compilation mode does some caching for markers in files, but it gets confused 61 | ;; because ghcid reloads the files in the same process. Here we parse the 62 | ;; 'Reloading...' message from ghcid and flush the cache for the mentioned 63 | ;; files. This approach is very similar to the 'omake' hacks included in 64 | ;; compilation mode. 65 | (add-to-list 66 | 'compilation-error-regexp-alist-alist 67 | '(ghcid-reloading 68 | "Reloading\\.\\.\\.\\(\\(\n .+\\)*\\)" 1 nil nil nil nil 69 | (0 (progn 70 | (let* ((filenames (cdr (split-string (match-string 1) "\n ")))) 71 | (dolist (filename filenames) 72 | (compilation--flush-file-structure filename))) 73 | nil)) 74 | )) 75 | (add-to-list 'compilation-error-regexp-alist 'ghcid-reloading) 76 | 77 | 78 | (defun ghcid-buffer-name () 79 | (concat "*" ghcid-process-name "*")) 80 | 81 | (defun ghcid-stack-cmd (target) 82 | (format "stack ghci %s --test --bench --ghci-options=-fno-code" target)) 83 | 84 | ;; TODO Pass in compilation command like compilation-mode 85 | (defun ghcid-command (h) 86 | (format "ghcid -c \"%s\" -h %s\n" (ghcid-stack-cmd ghcid-target) h)) 87 | 88 | (defun ghcid-get-buffer () 89 | "Create or reuse a ghcid buffer with the configured name and 90 | display it. Return the window that shows the buffer. 91 | 92 | User configuration will influence where the buffer gets shown 93 | exactly. See `ghcid-mode'." 94 | (display-buffer (get-buffer-create (ghcid-buffer-name)) '((display-buffer-reuse-window)))) 95 | 96 | (defun ghcid-start (dir) 97 | "Start ghcid in the specified directory" 98 | 99 | (with-selected-window (ghcid-get-buffer) 100 | 101 | (setq next-error-last-buffer (current-buffer)) 102 | (setq-local default-directory dir) 103 | 104 | ;; Only now we can figure out the height to pass along to the ghcid process 105 | (let ((height (- (window-body-size) 1))) 106 | 107 | (term-mode) 108 | (term-line-mode) ;; Allows easy navigation through the buffer 109 | (ghcid-mode) 110 | 111 | (setq-local term-buffer-maximum-size height) 112 | (setq-local scroll-up-aggressively 1) 113 | (setq-local show-trailing-whitespace nil) 114 | 115 | (term-exec (ghcid-buffer-name) 116 | ghcid-process-name 117 | "/bin/bash" 118 | nil 119 | (list "-c" (ghcid-command height))) 120 | 121 | ))) 122 | 123 | (defun ghcid-kill () 124 | (let* ((ghcid-buf (get-buffer (ghcid-buffer-name))) 125 | (ghcid-proc (get-buffer-process ghcid-buf))) 126 | (when (processp ghcid-proc) 127 | (progn 128 | (set-process-query-on-exit-flag ghcid-proc nil) 129 | (kill-process ghcid-proc) 130 | )))) 131 | 132 | ;; TODO Close stuff if it fails 133 | (defun ghcid () 134 | "Start a ghcid process in a new window. Kills any existing sessions. 135 | 136 | The process will be started in the directory of the buffer where 137 | you ran this command from." 138 | (interactive) 139 | (ghcid-start default-directory)) 140 | 141 | ;; Assumes that only one window is open 142 | (defun ghcid-stop () 143 | "Stop ghcid" 144 | (interactive) 145 | (ghcid-kill) 146 | (kill-buffer (ghcid-buffer-name))) 147 | 148 | 149 | (provide 'ghcid) 150 | 151 | ;;; ghcid.el ends here 152 | -------------------------------------------------------------------------------- /plugins/nvim/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Alexis Sellier 2016. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Neil Mitchell nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /plugins/nvim/README.md: -------------------------------------------------------------------------------- 1 | # Ghcid neovim plugin 2 | 3 | Provides instant haskell error feedback inside of neovim. 4 | This should be a lot faster than running neomake with ghc-mod, and 5 | also a lot simpler. Developed and maintained by @cloudhead. 6 | 7 | ![Obligatory gif][1] 8 | 9 | [1]: https://github.com/cloudhead/images/raw/master/neovim-ghcid.gif 10 | 11 | ### Requirements 12 | 13 | * neovim >= 0.2.1 (https://github.com/neovim/neovim) 14 | * ghcid >= 0.7 15 | 16 | ### Installation 17 | 18 | If you're using vim-plug, then add the following line to your list of plugins: 19 | 20 | Plug 'ndmitchell/ghcid', { 'rtp': 'plugins/nvim' } 21 | 22 | Then run `:PlugInstall`. 23 | 24 | For vundle, add the following: 25 | 26 | Plugin 'ndmitchell/ghcid', { 'rtp': 'plugins/nvim' } 27 | 28 | Then run `:PluginInstall`. 29 | 30 | Alternatively, copy the files in this folder to to your .vim directory. 31 | 32 | ### Usage 33 | 34 | `:Ghcid` runs ghcid inside a neovim terminal buffer and populates 35 | the quickfix list with any errors or warnings. You can call `:Ghcid` 36 | to toggle the window. 37 | 38 | After every file save, the quickfix list is updated with the output 39 | of ghcid. 40 | 41 | `:GhcidKill` kills the ghcid job. 42 | 43 | If you need to pass arguments to ghcid, simply pass them to the command, for 44 | example: 45 | 46 | :Ghcid -c cabal new-repl 47 | 48 | -------------------------------------------------------------------------------- /plugins/nvim/doc/neovim-ghcid.txt: -------------------------------------------------------------------------------- 1 | *neovim-ghcid.txt* Neovim wrapper for ghcid 2 | 3 | Version: 0.1 4 | Author: Alexis Sellier 5 | License: MIT 6 | 7 | USAGE *neovim-ghcid* 8 | 9 | Run this command to start Ghcid in a terminal buffer, 10 | This opens the buffer with filetype "ghcid". 11 | > 12 | :Ghcid 13 | < 14 | When ghcid reports an error, use ":cc" to jump to it, and ":cn" to jump to the 15 | next one. 16 | 17 | To kill ghcid: 18 | > 19 | :GhcidKill 20 | < 21 | VARIABLES *neovim-ghcid-variables* 22 | 23 | g:ghcid_keep_open number (default `!&hidden`) 24 | 25 | Keep the ghcid window open even if there are no errors. Defaults to `0` if 26 | 'hidden' is set, and `1` if it is not. Using a value of `0` without 'hidden' 27 | is not recommended. 28 | 29 | vim:tw=78:ts=8:ft=help:norl:noet:fen: 30 | -------------------------------------------------------------------------------- /plugins/nvim/ftplugin/haskell.vim: -------------------------------------------------------------------------------- 1 | " 2 | " neovim-ghcid 3 | " 4 | " Author: Alexis Sellier 5 | " Version: 0.1 6 | 7 | if exists("g:loaded_ghcid") || &cp || !has('nvim') 8 | finish 9 | endif 10 | let g:loaded_ghcid = 1 11 | 12 | if !exists("g:ghcid_size") 13 | if !exists("g:ghcid_length") 14 | let g:ghcid_size = 10 15 | else 16 | let g:ghcid_size = g:ghcid_length 17 | endif 18 | endif 19 | 20 | if !exists("g:ghcid_keep_open") 21 | let g:ghcid_keep_open = !&hidden 22 | endif 23 | 24 | if !exists("g:ghcid_command") 25 | let g:ghcid_command = "ghcid" 26 | endif 27 | 28 | if !exists("g:ghcid_background") 29 | let g:ghcid_background = 0 30 | endif 31 | 32 | if !exists("g:ghcid_verbosity") 33 | let g:ghcid_verbosity = 2 34 | endif 35 | 36 | if !exists("g:ghcid_placement") 37 | let g:ghcid_placement = 'below' 38 | endif 39 | 40 | let s:ghcid_command_args = {} 41 | let s:ghcid_base_sign_id = 100 42 | let s:ghcid_sign_id = s:ghcid_base_sign_id 43 | let s:ghcid_job_id = 0 44 | let s:ghcid_error_header = {} 45 | let s:ghcid_win_id = -1 46 | let s:ghcid_buf_id = -1 47 | let s:ghcid_dirty = 0 48 | 49 | command! -nargs=* Ghcid call s:ghcid() 50 | command! GhcidKill call s:ghcid_kill() 51 | 52 | sign define ghcid-warning text=× texthl=WarningSign 53 | sign define ghcid-error text=× texthl=ErrorSign 54 | 55 | function! s:ghcid_winnr() 56 | return win_id2win(s:ghcid_win_id) 57 | endfunction 58 | 59 | function! s:ghcid_bufnr() 60 | return bufnr(s:ghcid_buf_id) 61 | endfunction 62 | 63 | function! s:ghcid_gotowin() 64 | call win_gotoid(s:ghcid_win_id) 65 | endfunction 66 | 67 | function! s:ghcid_allgood() 68 | return empty(getqflist()) 69 | endfunction 70 | 71 | function! s:ghcid_update_status() 72 | if s:ghcid_winnr() <= 0 73 | return 74 | endif 75 | 76 | let nerrs = len(getqflist()) 77 | let window = win_getid() 78 | 79 | if window != s:ghcid_winnr() 80 | call s:ghcid_gotowin() 81 | endif 82 | 83 | let b:ghcid_status = 'Ghcid' 84 | if nerrs > 0 85 | let b:ghcid_status = 'Ghcid: ' . string(nerrs) . ' message(s)' 86 | endif 87 | setlocal statusline=%{b:ghcid_status} 88 | 89 | if win_getid() != window 90 | call win_gotoid(window) 91 | endif 92 | endfunction 93 | 94 | function! s:ghcid_closewin() 95 | if !g:ghcid_keep_open 96 | call s:ghcid_closewin_force() 97 | endif 98 | endfunction 99 | 100 | function! s:ghcid_closewin_force() 101 | call s:ghcid_gotowin() 102 | quit 103 | endfunction 104 | 105 | function s:placement_cmd() 106 | if g:ghcid_placement == 'above' | return 'above' | endif 107 | if g:ghcid_placement == 'below' | return 'below' | endif 108 | if g:ghcid_placement == 'left' | return 'vertical topleft' | endif 109 | if g:ghcid_placement == 'right' | return 'vertical rightb' | endif 110 | endfunction 111 | 112 | function! s:ghcid_openwin() 113 | let buf = s:ghcid_bufnr() 114 | 115 | if buf > 0 116 | exe 'keepalt' 'above' g:ghcid_size . 'split' '#' . buf 117 | else 118 | exe 'keepalt' s:placement_cmd() g:ghcid_size . 'new' 119 | file ghcid 120 | endif 121 | 122 | let s:ghcid_win_id = win_getid() 123 | call s:ghcid_update_status() 124 | silent setlocal nobuflisted winfixheight filetype=ghcid 125 | normal! G 126 | echo 127 | endfunction 128 | 129 | autocmd BufWritePost,FileChangedShellPost *.hs call s:ghcid_bufwrite() 130 | 131 | function! s:ghcid_bufwrite() abort 132 | let s:ghcid_dirty = 1 133 | endfunction 134 | 135 | let s:ghcid_error_header_regexp1= 136 | \ '^\s*\([^\t\r\n:]\+\):\(\d\+\):\([0-9\-]\+\):\s*\(warning:\)\?' 137 | 138 | let s:ghcid_error_header_regexp2= 139 | \ '^\s*\([^\t\r\n:]\+\):(\(\d\+\),\(\d\+\))-(\d\+,\d\+):\s*\(warning:\)\?' 140 | 141 | let s:ghcid_error_text_regexp= 142 | \ '\s\+\([^\t\r\n]\+\)' 143 | 144 | let s:ghcid_reloading_regexp= 145 | \ '^Reloading...' 146 | 147 | function! s:ghcid_parse_error_text(str) abort 148 | let result = matchlist(a:str, s:ghcid_error_text_regexp) 149 | if !len(result) 150 | return 151 | endif 152 | " Remove control characters and anything after. 153 | return substitute(result[1], "[[:cntrl:]].\*$", "", "g") 154 | endfunction 155 | 156 | function! s:ghcid_parse_error_header(str) abort 157 | let result = matchlist(a:str, s:ghcid_error_header_regexp1) 158 | if !len(result) 159 | let result = matchlist(a:str, s:ghcid_error_header_regexp2) 160 | if !len(result) 161 | return {} 162 | endif 163 | endif 164 | 165 | let file = result[1] 166 | let lnum = result[2] 167 | let col = result[3] 168 | let warn = result[4] 169 | 170 | " Find buffer after making file path relative to cd. 171 | " If the buffer isn't valid, vim will use the 'filename' entry. 172 | let efile = fnamemodify(expand(file), ':.') 173 | 174 | " Not a valid filename. 175 | if empty(efile) || !filereadable(efile) 176 | return {} 177 | endif 178 | 179 | let entry = { 'type': 'E', 180 | \ 'filename': efile, 181 | \ 'lnum': str2nr(lnum), 182 | \ 'col': str2nr(col), 183 | \ 'warning': !empty(warn) } 184 | 185 | let buf = bufnr(efile) 186 | if buf > 0 187 | let entry.bufnr = buf 188 | endif 189 | 190 | return entry 191 | endfunction 192 | 193 | function! s:ghcid_add_to_qflist(e) 194 | let old = getqflist() 195 | let new = [] 196 | 197 | " Create a new qflist based on the old one, but don't include the error 198 | " passed in. Effectively this replaces errors. 199 | for i in old 200 | if has_key(i, 'bufnr') && has_key(a:e, 'bufnr') && 201 | \ i.lnum == a:e.lnum && i.bufnr == a:e.bufnr && 202 | \ i.col == a:e.col 203 | continue 204 | endif 205 | 206 | call insert(new, i) 207 | endfor 208 | call setqflist(new + [a:e], 'r') 209 | endfunction 210 | 211 | function! s:ghcid_update(ghcid, data) abort 212 | let data = copy(a:data) 213 | 214 | if s:ghcid_dirty 215 | let s:ghcid_dirty = 0 216 | call s:ghcid_clear_signs() 217 | endif 218 | 219 | " If we see 'All good', then there are no errors and we 220 | " can safely close the ghcid window and reset the qflist. 221 | if !empty(matchstr(join(data), "All good")) 222 | if s:ghcid_winnr() 223 | call s:ghcid_closewin() 224 | endif 225 | if g:ghcid_verbosity > 1 226 | echo "Ghcid: OK" 227 | endif 228 | call s:ghcid_clear_signs() 229 | call s:ghcid_update_status() 230 | return 231 | endif 232 | 233 | " When ghcid reloads, clear all current errors. 234 | if !empty(matchstr(join(data), s:ghcid_reloading_regexp)) 235 | call s:ghcid_clear_signs() 236 | endif 237 | 238 | " Try to parse an error header string. If it succeeds, set the top-level 239 | " variable to the result. 240 | let error_header = s:ghcid_error_header 241 | if empty(error_header) 242 | while !empty(data) 243 | " NOTE: It's possible that the error message is on the same line as the 244 | " header, in which case it would be lost here. This doesn't seem to be 245 | " a problem though as the error is eventually output on its own line, 246 | " since ghcid's output is redundant. 247 | let error_header = s:ghcid_parse_error_header(data[0]) 248 | let data = data[1:] 249 | 250 | if !empty(error_header) 251 | let s:ghcid_error_header = error_header 252 | break 253 | endif 254 | endwhile 255 | 256 | " If we haven't found a header and there is nothing left to parse, 257 | " there's nothing left to do. 258 | if empty(error_header) || empty(data) 259 | return 260 | endif 261 | endif 262 | 263 | let error = copy(error_header) 264 | 265 | while !empty(data) 266 | " Try to parse the error text. If we got to this point, we have 267 | " an error header and some data left to parse. 268 | let error_text = s:ghcid_parse_error_text(data[0]) 269 | let data = data[1:] 270 | 271 | if !empty(error_text) 272 | let error.text = error_text 273 | let error.valid = 1 274 | let s:ghcid_error_header = {} 275 | break 276 | endif 277 | endwhile 278 | 279 | if has_key(error, 'valid') 280 | call s:ghcid_add_to_qflist(error) 281 | endif 282 | call s:ghcid_update_status() 283 | 284 | " Since we got here, we must have a valid error. 285 | " Open the ghcid window. 286 | if !s:ghcid_winnr() 287 | if g:ghcid_background && g:ghcid_verbosity > 0 288 | echo "Ghcid: " . len(getqflist()) . " message(s)" 289 | else 290 | call s:ghcid_openwin() 291 | wincmd p 292 | endif 293 | endif 294 | 295 | try 296 | silent exe "sign" 297 | \ "place" 298 | \ s:ghcid_sign_id 299 | \ "line=" . error.lnum 300 | \ "name=" . (error.warning ? "ghcid-warning" : "ghcid-error") 301 | \ "file=" . error.filename 302 | 303 | let s:ghcid_sign_id += 1 304 | catch 305 | " TODO: Sometimes the buffer name we have here is invalid so we can't 306 | " place a sign. Not sure how to fix this at the moment. 307 | endtry 308 | 309 | return data 310 | endfunction 311 | 312 | function! s:ghcid_clear_signs() abort 313 | for i in range(s:ghcid_base_sign_id, s:ghcid_sign_id) 314 | silent exe 'sign' 'unplace' i 315 | endfor 316 | let s:ghcid_sign_id = s:ghcid_base_sign_id 317 | 318 | " Clear the quickfix list. 319 | call setqflist([]) 320 | endfunction 321 | 322 | function! s:ghcid(...) abort 323 | let opts = {} 324 | let s:ghcid_killcmd = 0 325 | let s:ghcid_command_args = a:000 + ['--color=never'] 326 | 327 | if s:ghcid_winnr() > 0 328 | call s:ghcid_closewin_force() 329 | return 330 | endif 331 | 332 | function! opts.on_exit(id, code, event) 333 | if a:code != 0 && !s:ghcid_killcmd 334 | echoerr "Ghcid: Exited with status " . a:code 335 | call s:ghcid_stop() 336 | endif 337 | endfunction 338 | 339 | function! opts.on_stdout(id, data, event) abort 340 | let data = a:data 341 | 342 | while !empty(data) 343 | let data = s:ghcid_update(self, data) 344 | endwhile 345 | endfunction 346 | 347 | call s:ghcid_openwin() 348 | if s:ghcid_bufnr() <= 0 349 | call termopen(g:ghcid_command . " " . join(s:ghcid_command_args, ' '), opts) 350 | silent normal! G 351 | set norelativenumber 352 | set nonumber 353 | set syntax=haskell 354 | let s:ghcid_job_id = b:terminal_job_id 355 | endif 356 | 357 | let s:ghcid_buf_id = bufnr('%') 358 | wincmd p 359 | endfunction 360 | 361 | function! s:ghcid_kill() abort 362 | if s:ghcid_bufnr() > 0 363 | call s:ghcid_stop() 364 | echo "Ghcid: Killed" 365 | else 366 | echo "Ghcid: Not running" 367 | endif 368 | endfunction 369 | 370 | function! s:ghcid_stop() abort 371 | let s:ghcid_killcmd = 1 372 | silent exe 'bwipeout!' s:ghcid_bufnr() 373 | let s:ghcid_buf_id = -1 374 | let s:ghcid_win_id = -1 375 | let s:ghcid_job_id = -1 376 | endfunction 377 | -------------------------------------------------------------------------------- /plugins/vscode/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules -------------------------------------------------------------------------------- /plugins/vscode/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "${workspaceRoot}/../../test/foo", "${workspaceRoot}/../../test/foo/Root.hs" ], 11 | "stopOnEntry": false, 12 | "sourceMaps": true, 13 | "outFiles": [ "${workspaceRoot}/out/src/**/*.js" ], 14 | "preLaunchTask": "npm" 15 | }, 16 | { 17 | "name": "Launch Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "runtimeExecutable": "${execPath}", 21 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], 22 | "stopOnEntry": false, 23 | "sourceMaps": true, 24 | "outFiles": [ "${workspaceRoot}/out/test/**/*.js" ], 25 | "preLaunchTask": "npm" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /plugins/vscode/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | "[typescript]": { 10 | "editor.formatOnSave": false, 11 | }, 12 | "typescript.tsdk": "node_modules/typescript/lib" 13 | } -------------------------------------------------------------------------------- /plugins/vscode/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | // A task runner that calls a custom npm script that compiles the extension. 10 | { 11 | "version": "0.1.0", 12 | 13 | // we want to run npm 14 | "command": "npm", 15 | 16 | // the command is a shell script 17 | "isShellCommand": true, 18 | 19 | // show the output window only if unrecognized errors occur. 20 | "showOutput": "silent", 21 | 22 | // we run the custom script "compile" as defined in package.json 23 | "args": ["run", "compile", "--loglevel", "silent"], 24 | 25 | // The tsc compiler is started in watching mode 26 | "isBackground": true, 27 | 28 | // use the standard tsc in watch mode problem matcher to find compile problems in the output. 29 | "problemMatcher": "$tsc-watch" 30 | } -------------------------------------------------------------------------------- /plugins/vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | test/** 5 | src/** 6 | **/*.map 7 | .gitignore 8 | tsconfig.json 9 | vsc-extension-quickstart.md 10 | -------------------------------------------------------------------------------- /plugins/vscode/README.md: -------------------------------------------------------------------------------- 1 | # VSCode haskell-ghcid 2 | 3 | Shows errors and warnings from [`ghcid`](https://github.com/ndmitchell/ghcid) in the Problems pane and inline as red squiggles in the editor. Updates when files are saved. 4 | 5 | ## Usage 6 | 7 | Simply run `ghcid -o ghcid.txt`! `-o` instructs `ghcid` to write its output to a file every time it recompiles your code. This extension will automatically find and watch that file for updates. 8 | 9 | ## Spawning `ghcid` in VS Code 10 | 11 | Alternatively, you can tell VS Code to spawn `ghcid` in an embedded terminal: 12 | 13 | * Get your project working so typing `ghcid` in the project root works. If you need to pass special flags to `ghcid`, create a `.ghcid` file in the project root with the extra flags, e.g. `--command=cabal repl` or similar. 14 | * Run the VS Code command (`Ctrl+Shift+P`) named "Start Ghcid". 15 | 16 | ## Requirements 17 | 18 | Requires [`ghcid`](https://github.com/ndmitchell/ghcid) to be installed and on your `$PATH`. 19 | 20 | ## Local installation 21 | 22 | Run: 23 | 24 | npm install 25 | npm install -g vsce 26 | rm haskell-ghcid-*.vsix 27 | vsce package 28 | code --install-extension haskell-ghcid-*.vsix 29 | 30 | ## Making releases of this extension 31 | 32 | * Create a personal token following [the instructions](https://code.visualstudio.com/api/working-with-extensions/publishing-extension#get-a-personal-access-token), which involves visiting [this page](https://ndmitchell.visualstudio.com/_usersSettings/tokens). 33 | * Run `vsce publish -p `. 34 | 35 | ## Authors 36 | 37 | * [**@ndmitchell**](https://github.com/ndmitchell) Neil Mitchell 38 | * [**@chrismwendt**](https://github.com/chrismwendt) Chris Wendt 39 | -------------------------------------------------------------------------------- /plugins/vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "haskell-ghcid", 3 | "displayName": "haskell-ghcid", 4 | "description": "Integrate ghcid into VS Code", 5 | "version": "0.3.1", 6 | "publisher": "ndmitchell", 7 | "homepage": "https://github.com/ndmitchell/ghcid", 8 | "author": "Neil Mitchell, Chris Wendt", 9 | "engines": { 10 | "vscode": "^1.13.0" 11 | }, 12 | "license": "BSD-3-Clause", 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/ndmitchell/ghcid.git" 16 | }, 17 | "categories": [ 18 | "Programming Languages", 19 | "Linters" 20 | ], 21 | "//": [ 22 | "Activate unconditionally on startup to register file watchers.", 23 | "See https://github.com/ndmitchell/ghcid/pull/317" 24 | ], 25 | "activationEvents": [ 26 | "*" 27 | ], 28 | "main": "./out/src/extension", 29 | "contributes": { 30 | "commands": [ 31 | { 32 | "command": "extension.startGhcid", 33 | "title": "Start Ghcid" 34 | } 35 | ], 36 | "configuration": { 37 | "title": "Ghcid configuration", 38 | "properties": { 39 | "ghcid.command": { 40 | "type": "string", 41 | "default": "ghcid", 42 | "description": "Ghcid command" 43 | } 44 | } 45 | } 46 | }, 47 | "scripts": { 48 | "vscode:prepublish": "tsc -p ./", 49 | "compile": "tsc -watch -p ./", 50 | "postinstall": "node ./node_modules/vscode/bin/install", 51 | "test": "node ./node_modules/vscode/bin/test" 52 | }, 53 | "devDependencies": { 54 | "@types/mocha": "^2.2.48", 55 | "@types/node": "^6.14.9", 56 | "mocha": "^5.2.0", 57 | "typescript": "^3.8.3", 58 | "vsce": "^1.74.0", 59 | "vscode": "^1.1.26" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /plugins/vscode/src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as vscode from 'vscode'; 3 | import * as path from 'path'; 4 | import * as fs from 'fs'; 5 | import * as os from 'os'; 6 | import * as crypto from 'crypto' 7 | 8 | function pair(a : a, b : b) : [a,b] {return [a,b];} 9 | 10 | export function parseGhcidOutput(dir : string, s : string) : [vscode.Uri, vscode.Diagnostic][] { 11 | // standard lines, dealing with \r\n as well 12 | function lines(s : string) : string[] { 13 | return s.replace('\r','').split('\n').filter(x => x != ""); 14 | } 15 | 16 | // After the file location, message bodies are indented (perhaps prefixed by a line number) 17 | function isMessageBody(x : string) { 18 | if (x.startsWith(" ")) 19 | return true; 20 | let sep = x.indexOf('|'); 21 | if (sep == -1) 22 | return false; 23 | return !isNaN(Number(x.substr(0, sep))); 24 | } 25 | 26 | // split into separate error messages, which all start at col 0 (no spaces) and are following by message bodies 27 | function split(xs : string[]) : string[][] { 28 | var cont = []; 29 | var res = []; 30 | for (let x of xs) { 31 | if (isMessageBody(x)) 32 | cont.push(x); 33 | else { 34 | if (cont.length > 0) res.push(cont); 35 | cont = [x]; 36 | } 37 | } 38 | if (cont.length > 0) res.push(cont); 39 | return res; 40 | } 41 | 42 | function clean(lines: string[]): string[] { 43 | const newlines: string[] = [] 44 | for (const line of lines) { 45 | if (/In the/.test(line)) break 46 | 47 | if (line.match(/\s*\|$/)) break 48 | if (line.match(/(\d+)?\s*\|/)) break 49 | 50 | newlines.push(line) 51 | } 52 | return newlines 53 | } 54 | 55 | function dedent(lines: string[]): string[] { 56 | const indentation = Math.min(...lines.filter(line => line !== '').map(line => line.match(/^\s*/)[0].length)) 57 | return lines.map(line => line.slice(indentation)) 58 | } 59 | 60 | function parse(xs : string[]) : [vscode.Uri, vscode.Diagnostic][] { 61 | let r1 = /(..[^:]+):([0-9]+):([0-9]+):/ 62 | let r2 = /(..[^:]+):([0-9]+):([0-9]+)-([0-9]+):/ 63 | let r3 = /(..[^:]+):\(([0-9]+),([0-9]+)\)-\(([0-9]+),([0-9]+)\):/ 64 | var m : RegExpMatchArray; 65 | let f = (l1,c1,l2,c2) => { 66 | let range = new vscode.Range(parseInt(m[l1])-1,parseInt(m[c1])-1,parseInt(m[l2])-1,parseInt(m[c2])); 67 | let file = vscode.Uri.file(path.isAbsolute(m[1]) ? m[1] : path.join(dir, m[1])); 68 | var s = xs[0].substring(m[0].length).trim(); 69 | let i = s.indexOf(':'); 70 | var sev = vscode.DiagnosticSeverity.Error; 71 | if (i !== -1) { 72 | if (s.substr(0, i).toLowerCase() == 'warning') 73 | sev = vscode.DiagnosticSeverity.Warning; 74 | s = s.substr(i+1).trim(); 75 | } 76 | let msg = [].concat(/^\s*$/.test(s) ? [] : [s], xs.slice(1)); 77 | return [pair(file, new vscode.Diagnostic(range, dedent(msg).join('\n'), sev))]; 78 | }; 79 | if (xs[0].startsWith("All good")) 80 | return []; 81 | if (m = xs[0].match(r1)) 82 | return f(2,3,2,3); 83 | if (m = xs[0].match(r2)) 84 | return f(2,3,2,4); 85 | if (m = xs[0].match(r3)) 86 | return f(2,3,4,5); 87 | return [[new vscode.Uri(), new vscode.Diagnostic(new vscode.Range(0,0,0,0), dedent(xs).join('\n'))]]; 88 | } 89 | return [].concat(... split(lines(s)).map(clean).map(parse)); 90 | } 91 | 92 | function groupDiagnostic(xs : [vscode.Uri, vscode.Diagnostic[]][]) : [vscode.Uri, vscode.Diagnostic[]][] { 93 | let seen = new Map(); 94 | for (var i = 0; i < xs.length; i++) { 95 | let key = xs[i][0].path; 96 | if (seen.has(key)) { 97 | let v = seen.get(key); 98 | v[2] = v[2].concat(xs[i][1]); 99 | } 100 | else 101 | seen.set(key, [i, xs[i][0], xs[i][1]]); 102 | } 103 | return Array.from(seen.values()).sort((a,b) => a[0] - b[0]).map(x => pair(x[1],x[2])); 104 | } 105 | 106 | function watchOutput(root : string, file : string) : fs.FSWatcher { 107 | let d = vscode.languages.createDiagnosticCollection('ghcid'); 108 | let go = () => { 109 | d.clear() 110 | d.set(groupDiagnostic(parseGhcidOutput(root, fs.readFileSync(file, "utf8")).map(x => pair(x[0], [x[1]])))); 111 | }; 112 | let watcher = fs.watch(file, go); 113 | go(); 114 | return watcher; 115 | } 116 | 117 | async function autoWatch(context: vscode.ExtensionContext) { 118 | // TODO support multiple roots 119 | const watcher = vscode.workspace.createFileSystemWatcher('**/ghcid.txt') 120 | context.subscriptions.push(watcher); 121 | const uri2diags = new Map() 122 | context.subscriptions.push({ dispose: () => Array.from(uri2diags.values()).forEach(diag => diag.dispose()) }); 123 | 124 | const onUpdate = (uri: vscode.Uri) => { 125 | const diags = uri2diags.get(uri.fsPath) || vscode.languages.createDiagnosticCollection() 126 | uri2diags.set(uri.fsPath, diags) 127 | diags.clear() 128 | diags.set(groupDiagnostic(parseGhcidOutput(path.dirname(uri.fsPath), fs.readFileSync(uri.fsPath, "utf8")).map(x => pair(x[0], [x[1]])))); 129 | } 130 | 131 | (await vscode.workspace.findFiles('**/ghcid.txt')).forEach(onUpdate) 132 | watcher.onDidCreate(onUpdate) 133 | watcher.onDidChange(onUpdate) 134 | watcher.onDidDelete(uri => { 135 | uri2diags.get(uri.fsPath)?.dispose() 136 | uri2diags.delete(uri.fsPath) 137 | }) 138 | } 139 | 140 | // this method is called when your extension is activated 141 | // your extension is activated the very first time the command is executed 142 | export async function activate(context: vscode.ExtensionContext) { 143 | // The command has been defined in the package.json file 144 | // Now provide the implementation of the command with registerCommand 145 | // The commandId parameter must match the command field in package.json 146 | 147 | // Pointer to the last running watcher, so we can undo it 148 | var oldWatcher : fs.FSWatcher = null; 149 | var oldTerminal : vscode.Terminal = null; 150 | 151 | let cleanup = () => { 152 | if (oldWatcher != null) 153 | oldWatcher.close(); 154 | oldWatcher = null; 155 | if (oldTerminal != null) 156 | oldTerminal.dispose(); 157 | oldTerminal = null; 158 | } 159 | context.subscriptions.push({dispose: cleanup}); 160 | 161 | let add = (name : string, act : () => fs.FSWatcher) => { 162 | let dispose = vscode.commands.registerCommand(name, () => { 163 | try { 164 | cleanup(); 165 | oldWatcher = act(); 166 | } 167 | catch (e) { 168 | console.error("Ghcid extension failed in " + name + ": " + e); 169 | throw e; 170 | } 171 | }); 172 | context.subscriptions.push(dispose); 173 | } 174 | 175 | add('extension.startGhcid', () => { 176 | if (!vscode.workspace.rootPath) { 177 | vscode.window.showWarningMessage("You must open a workspace first.") 178 | return null; 179 | } 180 | // hashing the rootPath ensures we create a finite number of temp files 181 | var hash = crypto.createHash('sha256').update(vscode.workspace.rootPath).digest('hex').substring(0, 20); 182 | let file = path.join(os.tmpdir(), "ghcid-" + hash + ".txt"); 183 | context.subscriptions.push({dispose: () => {try {fs.unlinkSync(file);} catch (e) {};}}); 184 | fs.writeFileSync(file, ""); 185 | 186 | let ghcidCommand : string = vscode.workspace.getConfiguration('ghcid').get('command'); 187 | 188 | let opts : vscode.TerminalOptions = 189 | os.type().startsWith("Windows") ? 190 | {shellPath: "cmd.exe", shellArgs: ["/k", ghcidCommand]} : 191 | {shellPath: ghcidCommand, shellArgs: []}; 192 | opts.name = "ghcid"; 193 | opts.shellArgs.push("--outputfile=" + file); 194 | oldTerminal = vscode.window.createTerminal(opts); 195 | oldTerminal.show(); 196 | return watchOutput(vscode.workspace.rootPath, file); 197 | }); 198 | 199 | await autoWatch(context) 200 | } 201 | 202 | // this method is called when your extension is deactivated 203 | export function deactivate() { 204 | } 205 | -------------------------------------------------------------------------------- /plugins/vscode/test/extension.test.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Note: This example test is leveraging the Mocha test framework. 3 | // Please refer to their documentation on https://mochajs.org/ for help. 4 | // 5 | 6 | // The module 'assert' provides assertion methods from node 7 | import * as assert from 'assert'; 8 | 9 | // You can import and use all API from the 'vscode' module 10 | // as well as import your extension to test it 11 | import * as vscode from 'vscode'; 12 | import * as myExtension from '../src/extension'; 13 | 14 | // Defines a Mocha test suite to group tests of similar kind together 15 | suite("Extension Tests", () => { 16 | 17 | // Defines a Mocha unit test 18 | test("parseGhcidOutput", () => { 19 | let src = 20 | ["src\\Test.hs:81:11: error:" 21 | ," * No instance for (Num (IO [String])) arising from a use of `+'" 22 | ," * In a stmt of a 'do' block: xs <- getArgs + getArgs" 23 | ,"src\\General\\Binary.hs:15:1-22: warning: [-Wunused-imports]" 24 | ," The import of `Data.List.Extra' is redundant" 25 | ,"src\\General\\Binary.hs:17:1-23: warning: [-Wunused-imports]" 26 | ," The import of `Data.Tuple.Extra' is redundant" 27 | ,"C:\\src\\Development\\Shake\\Internal\\FileInfo.hs:(15,1)-(16,23): warning: [-Wunused-imports]" 28 | ," The import of `GHC.IO.Exception' is redundant"]; 29 | let want = 30 | [["/src/Test.hs", [80,10,80,11], vscode.DiagnosticSeverity.Error] 31 | ,["/src/General/Binary.hs", [14,0,14,22], vscode.DiagnosticSeverity.Warning] 32 | ,["/src/General/Binary.hs", [16,0,16,23], vscode.DiagnosticSeverity.Warning] 33 | ,["/C:/src/Development/Shake/Internal/FileInfo.hs", [14,0,15,23], vscode.DiagnosticSeverity.Warning]]; 34 | let res = myExtension.parseGhcidOutput("", src.join("\r\n")); 35 | let got = res.map(x => 36 | [ x[0].path 37 | , [x[1].range.start.line, x[1].range.start.character, x[1].range.end.line, x[1].range.end.character] 38 | , x[1].severity]); 39 | assert.deepStrictEqual(got, want); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /plugins/vscode/test/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | var testRunner = require('vscode/lib/testrunner'); 14 | 15 | // You can directly control Mocha options by uncommenting the following lines 16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 17 | testRunner.configure({ 18 | ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) 19 | useColors: true // colored output from test results 20 | }); 21 | 22 | module.exports = testRunner; -------------------------------------------------------------------------------- /plugins/vscode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "." 11 | }, 12 | "exclude": [ 13 | "node_modules", 14 | ".vscode-test" 15 | ] 16 | } -------------------------------------------------------------------------------- /plugins/vscode/vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your first VS Code Extension 2 | 3 | ## What's in the folder 4 | * This folder contains all of the files necessary for your extension 5 | * `package.json` - this is the manifest file in which you declare your extension and command. 6 | The sample plugin registers a command and defines its title and command name. With this information 7 | VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | The file exports one function, `activate`, which is called the very first time your extension is 10 | activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 11 | We pass the function containing the implementation of the command as the second parameter to 12 | `registerCommand`. 13 | 14 | ## Get up and running straight away 15 | * press `F5` to open a new window with your extension loaded 16 | * run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World` 17 | * set breakpoints in your code inside `src/extension.ts` to debug your extension 18 | * find output from your extension in the debug console 19 | 20 | ## Make changes 21 | * you can relaunch the extension from the debug toolbar after changing code in `src/extension.ts` 22 | * you can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes 23 | 24 | ## Explore the API 25 | * you can open the full set of our API when you open the file `node_modules/vscode/vscode.d.ts` 26 | 27 | ## Run tests 28 | * open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Launch Tests` 29 | * press `F5` to run the tests in a new window with your extension loaded 30 | * see the output of the test result in the debug console 31 | * make changes to `test/extension.test.ts` or create new test files inside the `test` folder 32 | * by convention, the test runner will only consider files matching the name pattern `**.test.ts` 33 | * you can create folders inside the `test` folder to structure your tests any way you want -------------------------------------------------------------------------------- /plugins/vscode/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/mocha@^2.2.48": 6 | version "2.2.48" 7 | resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.48.tgz#3523b126a0b049482e1c3c11877460f76622ffab" 8 | integrity sha512-nlK/iyETgafGli8Zh9zJVCTicvU3iajSkRwOh3Hhiva598CMqNJ4NcVCGMTGKpGpTYj/9R8RLzS9NAykSSCqGw== 9 | 10 | "@types/node@*": 11 | version "13.9.3" 12 | resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.3.tgz#6356df2647de9eac569f9a52eda3480fa9e70b4d" 13 | integrity sha512-01s+ac4qerwd6RHD+mVbOEsraDHSgUaefQlEdBbUolnQFjKwCr7luvAlEwW1RFojh67u0z4OUTjPn9LEl4zIkA== 14 | 15 | "@types/node@^6.14.9": 16 | version "6.14.13" 17 | resolved "https://registry.yarnpkg.com/@types/node/-/node-6.14.13.tgz#b6649578fc0b5dac88c4ef48a46cab33c50a6c72" 18 | integrity sha512-J1F0XJ/9zxlZel5ZlbeSuHW2OpabrUAqpFuC2sm2I3by8sERQ8+KCjNKUcq8QHuzpGMWiJpo9ZxeHrqrP2KzQw== 19 | 20 | agent-base@4, agent-base@^4.3.0: 21 | version "4.3.0" 22 | resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" 23 | integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== 24 | dependencies: 25 | es6-promisify "^5.0.0" 26 | 27 | ajv@^6.5.5: 28 | version "6.12.6" 29 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" 30 | integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== 31 | dependencies: 32 | fast-deep-equal "^3.1.1" 33 | fast-json-stable-stringify "^2.0.0" 34 | json-schema-traverse "^0.4.1" 35 | uri-js "^4.2.2" 36 | 37 | ansi-styles@^3.2.1: 38 | version "3.2.1" 39 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 40 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 41 | dependencies: 42 | color-convert "^1.9.0" 43 | 44 | argparse@^1.0.7: 45 | version "1.0.10" 46 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" 47 | integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== 48 | dependencies: 49 | sprintf-js "~1.0.2" 50 | 51 | asn1@~0.2.3: 52 | version "0.2.4" 53 | resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" 54 | integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== 55 | dependencies: 56 | safer-buffer "~2.1.0" 57 | 58 | assert-plus@1.0.0, assert-plus@^1.0.0: 59 | version "1.0.0" 60 | resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" 61 | integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= 62 | 63 | asynckit@^0.4.0: 64 | version "0.4.0" 65 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 66 | integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= 67 | 68 | aws-sign2@~0.7.0: 69 | version "0.7.0" 70 | resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" 71 | integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= 72 | 73 | aws4@^1.8.0: 74 | version "1.9.1" 75 | resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" 76 | integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== 77 | 78 | azure-devops-node-api@^7.2.0: 79 | version "7.2.0" 80 | resolved "https://registry.yarnpkg.com/azure-devops-node-api/-/azure-devops-node-api-7.2.0.tgz#131d4e01cf12ebc6e45569b5e0c5c249e4114d6d" 81 | integrity sha512-pMfGJ6gAQ7LRKTHgiRF+8iaUUeGAI0c8puLaqHLc7B8AR7W6GJLozK9RFeUHFjEGybC9/EB3r67WPd7e46zQ8w== 82 | dependencies: 83 | os "0.1.1" 84 | tunnel "0.0.4" 85 | typed-rest-client "1.2.0" 86 | underscore "1.8.3" 87 | 88 | balanced-match@^1.0.0: 89 | version "1.0.0" 90 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 91 | integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= 92 | 93 | bcrypt-pbkdf@^1.0.0: 94 | version "1.0.2" 95 | resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" 96 | integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= 97 | dependencies: 98 | tweetnacl "^0.14.3" 99 | 100 | boolbase@~1.0.0: 101 | version "1.0.0" 102 | resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" 103 | integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= 104 | 105 | brace-expansion@^1.1.7: 106 | version "1.1.11" 107 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 108 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 109 | dependencies: 110 | balanced-match "^1.0.0" 111 | concat-map "0.0.1" 112 | 113 | browser-stdout@1.3.1: 114 | version "1.3.1" 115 | resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" 116 | integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== 117 | 118 | buffer-crc32@~0.2.3: 119 | version "0.2.13" 120 | resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" 121 | integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= 122 | 123 | buffer-from@^1.0.0: 124 | version "1.1.1" 125 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" 126 | integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== 127 | 128 | caseless@~0.12.0: 129 | version "0.12.0" 130 | resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" 131 | integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= 132 | 133 | chalk@^2.4.2: 134 | version "2.4.2" 135 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" 136 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== 137 | dependencies: 138 | ansi-styles "^3.2.1" 139 | escape-string-regexp "^1.0.5" 140 | supports-color "^5.3.0" 141 | 142 | cheerio@^1.0.0-rc.1: 143 | version "1.0.0-rc.3" 144 | resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6" 145 | integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA== 146 | dependencies: 147 | css-select "~1.2.0" 148 | dom-serializer "~0.1.1" 149 | entities "~1.1.1" 150 | htmlparser2 "^3.9.1" 151 | lodash "^4.15.0" 152 | parse5 "^3.0.1" 153 | 154 | color-convert@^1.9.0: 155 | version "1.9.3" 156 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 157 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 158 | dependencies: 159 | color-name "1.1.3" 160 | 161 | color-name@1.1.3: 162 | version "1.1.3" 163 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 164 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= 165 | 166 | combined-stream@^1.0.6, combined-stream@~1.0.6: 167 | version "1.0.8" 168 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 169 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 170 | dependencies: 171 | delayed-stream "~1.0.0" 172 | 173 | commander@2.15.1: 174 | version "2.15.1" 175 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" 176 | integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== 177 | 178 | commander@^2.8.1: 179 | version "2.20.3" 180 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" 181 | integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== 182 | 183 | concat-map@0.0.1: 184 | version "0.0.1" 185 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 186 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 187 | 188 | core-util-is@1.0.2: 189 | version "1.0.2" 190 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 191 | integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= 192 | 193 | css-select@~1.2.0: 194 | version "1.2.0" 195 | resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" 196 | integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= 197 | dependencies: 198 | boolbase "~1.0.0" 199 | css-what "2.1" 200 | domutils "1.5.1" 201 | nth-check "~1.0.1" 202 | 203 | css-what@2.1: 204 | version "2.1.3" 205 | resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" 206 | integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== 207 | 208 | dashdash@^1.12.0: 209 | version "1.14.1" 210 | resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" 211 | integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= 212 | dependencies: 213 | assert-plus "^1.0.0" 214 | 215 | debug@3.1.0: 216 | version "3.1.0" 217 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 218 | integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== 219 | dependencies: 220 | ms "2.0.0" 221 | 222 | debug@^3.1.0: 223 | version "3.2.6" 224 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" 225 | integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== 226 | dependencies: 227 | ms "^2.1.1" 228 | 229 | delayed-stream@~1.0.0: 230 | version "1.0.0" 231 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 232 | integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= 233 | 234 | denodeify@^1.2.1: 235 | version "1.2.1" 236 | resolved "https://registry.yarnpkg.com/denodeify/-/denodeify-1.2.1.tgz#3a36287f5034e699e7577901052c2e6c94251631" 237 | integrity sha1-OjYof1A05pnnV3kBBSwubJQlFjE= 238 | 239 | didyoumean@^1.2.1: 240 | version "1.2.1" 241 | resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.1.tgz#e92edfdada6537d484d73c0172fd1eba0c4976ff" 242 | integrity sha1-6S7f2tplN9SE1zwBcv0eugxJdv8= 243 | 244 | diff@3.5.0: 245 | version "3.5.0" 246 | resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" 247 | integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== 248 | 249 | dom-serializer@0: 250 | version "0.2.2" 251 | resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" 252 | integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== 253 | dependencies: 254 | domelementtype "^2.0.1" 255 | entities "^2.0.0" 256 | 257 | dom-serializer@~0.1.1: 258 | version "0.1.1" 259 | resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" 260 | integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== 261 | dependencies: 262 | domelementtype "^1.3.0" 263 | entities "^1.1.1" 264 | 265 | domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: 266 | version "1.3.1" 267 | resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" 268 | integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== 269 | 270 | domelementtype@^2.0.1: 271 | version "2.0.1" 272 | resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" 273 | integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== 274 | 275 | domhandler@^2.3.0: 276 | version "2.4.2" 277 | resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" 278 | integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== 279 | dependencies: 280 | domelementtype "1" 281 | 282 | domutils@1.5.1: 283 | version "1.5.1" 284 | resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" 285 | integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= 286 | dependencies: 287 | dom-serializer "0" 288 | domelementtype "1" 289 | 290 | domutils@^1.5.1: 291 | version "1.7.0" 292 | resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" 293 | integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== 294 | dependencies: 295 | dom-serializer "0" 296 | domelementtype "1" 297 | 298 | ecc-jsbn@~0.1.1: 299 | version "0.1.2" 300 | resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" 301 | integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= 302 | dependencies: 303 | jsbn "~0.1.0" 304 | safer-buffer "^2.1.0" 305 | 306 | entities@^1.1.1, entities@~1.1.1: 307 | version "1.1.2" 308 | resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" 309 | integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== 310 | 311 | entities@^2.0.0: 312 | version "2.0.0" 313 | resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" 314 | integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== 315 | 316 | es6-promise@^4.0.3: 317 | version "4.2.8" 318 | resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" 319 | integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== 320 | 321 | es6-promisify@^5.0.0: 322 | version "5.0.0" 323 | resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" 324 | integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= 325 | dependencies: 326 | es6-promise "^4.0.3" 327 | 328 | escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: 329 | version "1.0.5" 330 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 331 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= 332 | 333 | extend@~3.0.2: 334 | version "3.0.2" 335 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" 336 | integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== 337 | 338 | extsprintf@1.3.0: 339 | version "1.3.0" 340 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" 341 | integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= 342 | 343 | extsprintf@^1.2.0: 344 | version "1.4.0" 345 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" 346 | integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= 347 | 348 | fast-deep-equal@^3.1.1: 349 | version "3.1.3" 350 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" 351 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 352 | 353 | fast-json-stable-stringify@^2.0.0: 354 | version "2.1.0" 355 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" 356 | integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== 357 | 358 | fd-slicer@~1.1.0: 359 | version "1.1.0" 360 | resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" 361 | integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= 362 | dependencies: 363 | pend "~1.2.0" 364 | 365 | forever-agent@~0.6.1: 366 | version "0.6.1" 367 | resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" 368 | integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= 369 | 370 | form-data@~2.3.2: 371 | version "2.3.3" 372 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" 373 | integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== 374 | dependencies: 375 | asynckit "^0.4.0" 376 | combined-stream "^1.0.6" 377 | mime-types "^2.1.12" 378 | 379 | fs.realpath@^1.0.0: 380 | version "1.0.0" 381 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 382 | integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= 383 | 384 | getpass@^0.1.1: 385 | version "0.1.7" 386 | resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" 387 | integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= 388 | dependencies: 389 | assert-plus "^1.0.0" 390 | 391 | glob@7.1.2: 392 | version "7.1.2" 393 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 394 | integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ== 395 | dependencies: 396 | fs.realpath "^1.0.0" 397 | inflight "^1.0.4" 398 | inherits "2" 399 | minimatch "^3.0.4" 400 | once "^1.3.0" 401 | path-is-absolute "^1.0.0" 402 | 403 | glob@^7.0.6, glob@^7.1.2: 404 | version "7.1.6" 405 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" 406 | integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== 407 | dependencies: 408 | fs.realpath "^1.0.0" 409 | inflight "^1.0.4" 410 | inherits "2" 411 | minimatch "^3.0.4" 412 | once "^1.3.0" 413 | path-is-absolute "^1.0.0" 414 | 415 | growl@1.10.5: 416 | version "1.10.5" 417 | resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" 418 | integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== 419 | 420 | har-schema@^2.0.0: 421 | version "2.0.0" 422 | resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" 423 | integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= 424 | 425 | har-validator@~5.1.3: 426 | version "5.1.3" 427 | resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" 428 | integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== 429 | dependencies: 430 | ajv "^6.5.5" 431 | har-schema "^2.0.0" 432 | 433 | has-flag@^3.0.0: 434 | version "3.0.0" 435 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 436 | integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= 437 | 438 | he@1.1.1: 439 | version "1.1.1" 440 | resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" 441 | integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= 442 | 443 | htmlparser2@^3.9.1: 444 | version "3.10.1" 445 | resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" 446 | integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== 447 | dependencies: 448 | domelementtype "^1.3.1" 449 | domhandler "^2.3.0" 450 | domutils "^1.5.1" 451 | entities "^1.1.1" 452 | inherits "^2.0.1" 453 | readable-stream "^3.1.1" 454 | 455 | http-proxy-agent@^2.1.0: 456 | version "2.1.0" 457 | resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" 458 | integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== 459 | dependencies: 460 | agent-base "4" 461 | debug "3.1.0" 462 | 463 | http-signature@~1.2.0: 464 | version "1.2.0" 465 | resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" 466 | integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= 467 | dependencies: 468 | assert-plus "^1.0.0" 469 | jsprim "^1.2.2" 470 | sshpk "^1.7.0" 471 | 472 | https-proxy-agent@^2.2.1: 473 | version "2.2.4" 474 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" 475 | integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== 476 | dependencies: 477 | agent-base "^4.3.0" 478 | debug "^3.1.0" 479 | 480 | inflight@^1.0.4: 481 | version "1.0.6" 482 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 483 | integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= 484 | dependencies: 485 | once "^1.3.0" 486 | wrappy "1" 487 | 488 | inherits@2, inherits@^2.0.1, inherits@^2.0.3: 489 | version "2.0.4" 490 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 491 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 492 | 493 | is-typedarray@~1.0.0: 494 | version "1.0.0" 495 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 496 | integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= 497 | 498 | isstream@~0.1.2: 499 | version "0.1.2" 500 | resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" 501 | integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= 502 | 503 | jsbn@~0.1.0: 504 | version "0.1.1" 505 | resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" 506 | integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= 507 | 508 | json-schema-traverse@^0.4.1: 509 | version "0.4.1" 510 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" 511 | integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== 512 | 513 | json-schema@0.2.3: 514 | version "0.2.3" 515 | resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" 516 | integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= 517 | 518 | json-stringify-safe@~5.0.1: 519 | version "5.0.1" 520 | resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" 521 | integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= 522 | 523 | jsprim@^1.2.2: 524 | version "1.4.1" 525 | resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" 526 | integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= 527 | dependencies: 528 | assert-plus "1.0.0" 529 | extsprintf "1.3.0" 530 | json-schema "0.2.3" 531 | verror "1.10.0" 532 | 533 | linkify-it@^2.0.0: 534 | version "2.2.0" 535 | resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.2.0.tgz#e3b54697e78bf915c70a38acd78fd09e0058b1cf" 536 | integrity sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw== 537 | dependencies: 538 | uc.micro "^1.0.1" 539 | 540 | lodash@^4.15.0, lodash@^4.17.15: 541 | version "4.17.21" 542 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 543 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 544 | 545 | markdown-it@^8.3.1: 546 | version "8.4.2" 547 | resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54" 548 | integrity sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ== 549 | dependencies: 550 | argparse "^1.0.7" 551 | entities "~1.1.1" 552 | linkify-it "^2.0.0" 553 | mdurl "^1.0.1" 554 | uc.micro "^1.0.5" 555 | 556 | mdurl@^1.0.1: 557 | version "1.0.1" 558 | resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" 559 | integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= 560 | 561 | mime-db@1.43.0: 562 | version "1.43.0" 563 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" 564 | integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== 565 | 566 | mime-types@^2.1.12, mime-types@~2.1.19: 567 | version "2.1.26" 568 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" 569 | integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== 570 | dependencies: 571 | mime-db "1.43.0" 572 | 573 | mime@^1.3.4: 574 | version "1.6.0" 575 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 576 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 577 | 578 | minimatch@3.0.4, minimatch@^3.0.3, minimatch@^3.0.4: 579 | version "3.0.4" 580 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 581 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 582 | dependencies: 583 | brace-expansion "^1.1.7" 584 | 585 | minimist@0.0.8: 586 | version "0.0.8" 587 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 588 | integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= 589 | 590 | mkdirp@0.5.1: 591 | version "0.5.1" 592 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 593 | integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= 594 | dependencies: 595 | minimist "0.0.8" 596 | 597 | mocha@^5.2.0: 598 | version "5.2.0" 599 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" 600 | integrity sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ== 601 | dependencies: 602 | browser-stdout "1.3.1" 603 | commander "2.15.1" 604 | debug "3.1.0" 605 | diff "3.5.0" 606 | escape-string-regexp "1.0.5" 607 | glob "7.1.2" 608 | growl "1.10.5" 609 | he "1.1.1" 610 | minimatch "3.0.4" 611 | mkdirp "0.5.1" 612 | supports-color "5.4.0" 613 | 614 | ms@2.0.0: 615 | version "2.0.0" 616 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 617 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 618 | 619 | ms@^2.1.1: 620 | version "2.1.2" 621 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 622 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 623 | 624 | mute-stream@~0.0.4: 625 | version "0.0.8" 626 | resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" 627 | integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== 628 | 629 | nth-check@~1.0.1: 630 | version "1.0.2" 631 | resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" 632 | integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== 633 | dependencies: 634 | boolbase "~1.0.0" 635 | 636 | oauth-sign@~0.9.0: 637 | version "0.9.0" 638 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" 639 | integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== 640 | 641 | once@^1.3.0: 642 | version "1.4.0" 643 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 644 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 645 | dependencies: 646 | wrappy "1" 647 | 648 | os-homedir@^1.0.0: 649 | version "1.0.2" 650 | resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" 651 | integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= 652 | 653 | os-tmpdir@^1.0.0, os-tmpdir@~1.0.1: 654 | version "1.0.2" 655 | resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" 656 | integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= 657 | 658 | os@0.1.1: 659 | version "0.1.1" 660 | resolved "https://registry.yarnpkg.com/os/-/os-0.1.1.tgz#208845e89e193ad4d971474b93947736a56d13f3" 661 | integrity sha1-IIhF6J4ZOtTZcUdLk5R3NqVtE/M= 662 | 663 | osenv@^0.1.3: 664 | version "0.1.5" 665 | resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" 666 | integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== 667 | dependencies: 668 | os-homedir "^1.0.0" 669 | os-tmpdir "^1.0.0" 670 | 671 | parse-semver@^1.1.1: 672 | version "1.1.1" 673 | resolved "https://registry.yarnpkg.com/parse-semver/-/parse-semver-1.1.1.tgz#9a4afd6df063dc4826f93fba4a99cf223f666cb8" 674 | integrity sha1-mkr9bfBj3Egm+T+6SpnPIj9mbLg= 675 | dependencies: 676 | semver "^5.1.0" 677 | 678 | parse5@^3.0.1: 679 | version "3.0.3" 680 | resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" 681 | integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA== 682 | dependencies: 683 | "@types/node" "*" 684 | 685 | path-is-absolute@^1.0.0: 686 | version "1.0.1" 687 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 688 | integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= 689 | 690 | pend@~1.2.0: 691 | version "1.2.0" 692 | resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" 693 | integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= 694 | 695 | performance-now@^2.1.0: 696 | version "2.1.0" 697 | resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" 698 | integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= 699 | 700 | psl@^1.1.28: 701 | version "1.7.0" 702 | resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" 703 | integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ== 704 | 705 | punycode@^2.1.0, punycode@^2.1.1: 706 | version "2.1.1" 707 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" 708 | integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== 709 | 710 | qs@~6.5.2: 711 | version "6.5.3" 712 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" 713 | integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== 714 | 715 | querystringify@^2.1.1: 716 | version "2.2.0" 717 | resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" 718 | integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== 719 | 720 | read@^1.0.7: 721 | version "1.0.7" 722 | resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" 723 | integrity sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ= 724 | dependencies: 725 | mute-stream "~0.0.4" 726 | 727 | readable-stream@^3.1.1: 728 | version "3.6.0" 729 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" 730 | integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== 731 | dependencies: 732 | inherits "^2.0.3" 733 | string_decoder "^1.1.1" 734 | util-deprecate "^1.0.1" 735 | 736 | request@^2.88.0: 737 | version "2.88.2" 738 | resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" 739 | integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== 740 | dependencies: 741 | aws-sign2 "~0.7.0" 742 | aws4 "^1.8.0" 743 | caseless "~0.12.0" 744 | combined-stream "~1.0.6" 745 | extend "~3.0.2" 746 | forever-agent "~0.6.1" 747 | form-data "~2.3.2" 748 | har-validator "~5.1.3" 749 | http-signature "~1.2.0" 750 | is-typedarray "~1.0.0" 751 | isstream "~0.1.2" 752 | json-stringify-safe "~5.0.1" 753 | mime-types "~2.1.19" 754 | oauth-sign "~0.9.0" 755 | performance-now "^2.1.0" 756 | qs "~6.5.2" 757 | safe-buffer "^5.1.2" 758 | tough-cookie "~2.5.0" 759 | tunnel-agent "^0.6.0" 760 | uuid "^3.3.2" 761 | 762 | requires-port@^1.0.0: 763 | version "1.0.0" 764 | resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" 765 | integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= 766 | 767 | safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: 768 | version "5.2.0" 769 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" 770 | integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== 771 | 772 | safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: 773 | version "2.1.2" 774 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 775 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 776 | 777 | semver@^5.1.0, semver@^5.4.1: 778 | version "5.7.1" 779 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" 780 | integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== 781 | 782 | source-map-support@^0.5.0: 783 | version "0.5.16" 784 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" 785 | integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== 786 | dependencies: 787 | buffer-from "^1.0.0" 788 | source-map "^0.6.0" 789 | 790 | source-map@^0.6.0: 791 | version "0.6.1" 792 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 793 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 794 | 795 | sprintf-js@~1.0.2: 796 | version "1.0.3" 797 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" 798 | integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= 799 | 800 | sshpk@^1.7.0: 801 | version "1.16.1" 802 | resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" 803 | integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== 804 | dependencies: 805 | asn1 "~0.2.3" 806 | assert-plus "^1.0.0" 807 | bcrypt-pbkdf "^1.0.0" 808 | dashdash "^1.12.0" 809 | ecc-jsbn "~0.1.1" 810 | getpass "^0.1.1" 811 | jsbn "~0.1.0" 812 | safer-buffer "^2.0.2" 813 | tweetnacl "~0.14.0" 814 | 815 | string_decoder@^1.1.1: 816 | version "1.3.0" 817 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" 818 | integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== 819 | dependencies: 820 | safe-buffer "~5.2.0" 821 | 822 | supports-color@5.4.0: 823 | version "5.4.0" 824 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" 825 | integrity sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w== 826 | dependencies: 827 | has-flag "^3.0.0" 828 | 829 | supports-color@^5.3.0: 830 | version "5.5.0" 831 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 832 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 833 | dependencies: 834 | has-flag "^3.0.0" 835 | 836 | tmp@0.0.29: 837 | version "0.0.29" 838 | resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.29.tgz#f25125ff0dd9da3ccb0c2dd371ee1288bb9128c0" 839 | integrity sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA= 840 | dependencies: 841 | os-tmpdir "~1.0.1" 842 | 843 | tough-cookie@~2.5.0: 844 | version "2.5.0" 845 | resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" 846 | integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== 847 | dependencies: 848 | psl "^1.1.28" 849 | punycode "^2.1.1" 850 | 851 | tunnel-agent@^0.6.0: 852 | version "0.6.0" 853 | resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" 854 | integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= 855 | dependencies: 856 | safe-buffer "^5.0.1" 857 | 858 | tunnel@0.0.4: 859 | version "0.0.4" 860 | resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.4.tgz#2d3785a158c174c9a16dc2c046ec5fc5f1742213" 861 | integrity sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM= 862 | 863 | tweetnacl@^0.14.3, tweetnacl@~0.14.0: 864 | version "0.14.5" 865 | resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" 866 | integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= 867 | 868 | typed-rest-client@1.2.0: 869 | version "1.2.0" 870 | resolved "https://registry.yarnpkg.com/typed-rest-client/-/typed-rest-client-1.2.0.tgz#723085d203f38d7d147271e5ed3a75488eb44a02" 871 | integrity sha512-FrUshzZ1yxH8YwGR29PWWnfksLEILbWJydU7zfIRkyH7kAEzB62uMAl2WY6EyolWpLpVHeJGgQm45/MaruaHpw== 872 | dependencies: 873 | tunnel "0.0.4" 874 | underscore "1.8.3" 875 | 876 | typescript@^3.8.3: 877 | version "3.8.3" 878 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" 879 | integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== 880 | 881 | uc.micro@^1.0.1, uc.micro@^1.0.5: 882 | version "1.0.6" 883 | resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" 884 | integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== 885 | 886 | underscore@1.8.3: 887 | version "1.8.3" 888 | resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" 889 | integrity sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI= 890 | 891 | uri-js@^4.2.2: 892 | version "4.4.1" 893 | resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" 894 | integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== 895 | dependencies: 896 | punycode "^2.1.0" 897 | 898 | url-join@^1.1.0: 899 | version "1.1.0" 900 | resolved "https://registry.yarnpkg.com/url-join/-/url-join-1.1.0.tgz#741c6c2f4596c4830d6718460920d0c92202dc78" 901 | integrity sha1-dBxsL0WWxIMNZxhGCSDQySIC3Hg= 902 | 903 | url-parse@^1.4.4: 904 | version "1.5.10" 905 | resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" 906 | integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== 907 | dependencies: 908 | querystringify "^2.1.1" 909 | requires-port "^1.0.0" 910 | 911 | util-deprecate@^1.0.1: 912 | version "1.0.2" 913 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 914 | integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= 915 | 916 | uuid@^3.3.2: 917 | version "3.4.0" 918 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" 919 | integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== 920 | 921 | verror@1.10.0: 922 | version "1.10.0" 923 | resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" 924 | integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= 925 | dependencies: 926 | assert-plus "^1.0.0" 927 | core-util-is "1.0.2" 928 | extsprintf "^1.2.0" 929 | 930 | vsce@^1.74.0: 931 | version "1.74.0" 932 | resolved "https://registry.yarnpkg.com/vsce/-/vsce-1.74.0.tgz#ed70a20d08c70e63d21b99fc35428c4c8efc0a82" 933 | integrity sha512-8zWM9bZBNn9my40kkxAxdY4nhb9ADfazXsyDgx1thbRaLPbmPTlmqQ55vCAyWYFEi6XbJv8w599vzVUqsU1gHg== 934 | dependencies: 935 | azure-devops-node-api "^7.2.0" 936 | chalk "^2.4.2" 937 | cheerio "^1.0.0-rc.1" 938 | commander "^2.8.1" 939 | denodeify "^1.2.1" 940 | didyoumean "^1.2.1" 941 | glob "^7.0.6" 942 | lodash "^4.17.15" 943 | markdown-it "^8.3.1" 944 | mime "^1.3.4" 945 | minimatch "^3.0.3" 946 | osenv "^0.1.3" 947 | parse-semver "^1.1.1" 948 | read "^1.0.7" 949 | semver "^5.1.0" 950 | tmp "0.0.29" 951 | typed-rest-client "1.2.0" 952 | url-join "^1.1.0" 953 | yauzl "^2.3.1" 954 | yazl "^2.2.2" 955 | 956 | vscode-test@^0.4.1: 957 | version "0.4.3" 958 | resolved "https://registry.yarnpkg.com/vscode-test/-/vscode-test-0.4.3.tgz#461ebf25fc4bc93d77d982aed556658a2e2b90b8" 959 | integrity sha512-EkMGqBSefZH2MgW65nY05rdRSko15uvzq4VAPM5jVmwYuFQKE7eikKXNJDRxL+OITXHB6pI+a3XqqD32Y3KC5w== 960 | dependencies: 961 | http-proxy-agent "^2.1.0" 962 | https-proxy-agent "^2.2.1" 963 | 964 | vscode@^1.1.26: 965 | version "1.1.36" 966 | resolved "https://registry.yarnpkg.com/vscode/-/vscode-1.1.36.tgz#5e1a0d1bf4977d0c7bc5159a9a13d5b104d4b1b6" 967 | integrity sha512-cGFh9jmGLcTapCpPCKvn8aG/j9zVQ+0x5hzYJq5h5YyUXVGa1iamOaB2M2PZXoumQPES4qeAP1FwkI0b6tL4bQ== 968 | dependencies: 969 | glob "^7.1.2" 970 | mocha "^5.2.0" 971 | request "^2.88.0" 972 | semver "^5.4.1" 973 | source-map-support "^0.5.0" 974 | url-parse "^1.4.4" 975 | vscode-test "^0.4.1" 976 | 977 | wrappy@1: 978 | version "1.0.2" 979 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 980 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 981 | 982 | yauzl@^2.3.1: 983 | version "2.10.0" 984 | resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" 985 | integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= 986 | dependencies: 987 | buffer-crc32 "~0.2.3" 988 | fd-slicer "~1.1.0" 989 | 990 | yazl@^2.2.2: 991 | version "2.5.1" 992 | resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.5.1.tgz#a3d65d3dd659a5b0937850e8609f22fffa2b5c35" 993 | integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw== 994 | dependencies: 995 | buffer-crc32 "~0.2.3" 996 | -------------------------------------------------------------------------------- /src/Ghcid.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE RecordWildCards, DeriveDataTypeable, TupleSections #-} 2 | {-# OPTIONS_GHC -fno-cse #-} 3 | 4 | -- | The application entry point 5 | module Ghcid(main, mainWithTerminal, TermSize(..), WordWrap(..)) where 6 | 7 | import Control.Exception 8 | import System.IO.Error 9 | import Control.Applicative 10 | import Control.Monad.Extra 11 | import Data.List.Extra 12 | import Data.Maybe 13 | import Data.Ord 14 | import Data.Tuple.Extra 15 | import Data.Version 16 | import Session 17 | import qualified System.Console.Terminal.Size as Term 18 | import System.Console.CmdArgs 19 | import System.Console.CmdArgs.Explicit 20 | import System.Console.ANSI 21 | import System.Environment 22 | import System.Directory.Extra 23 | import System.Time.Extra 24 | import System.Exit 25 | import System.FilePath 26 | import System.Process 27 | import System.Info 28 | import System.IO.Extra 29 | 30 | import Paths_ghcid 31 | import Language.Haskell.Ghcid.Escape 32 | import Language.Haskell.Ghcid.Terminal 33 | import Language.Haskell.Ghcid.Util 34 | import Language.Haskell.Ghcid.Types 35 | import Wait 36 | 37 | import Prelude 38 | 39 | 40 | -- | Command line options 41 | data Options = Options 42 | {command :: String 43 | ,arguments :: [String] 44 | ,test :: [String] 45 | ,test_message :: String 46 | ,run :: [String] 47 | ,warnings :: Bool 48 | ,lint :: Maybe String 49 | ,no_status :: Bool 50 | ,clear :: Bool 51 | ,reverse_errors :: Bool 52 | ,no_height_limit :: Bool 53 | ,height :: Maybe Int 54 | ,width :: Maybe Int 55 | ,topmost :: Bool 56 | ,no_title :: Bool 57 | ,project :: String 58 | ,reload :: [FilePath] 59 | ,restart :: [FilePath] 60 | ,directory :: FilePath 61 | ,outputfile :: [FilePath] 62 | ,ignoreLoaded :: Bool 63 | ,poll :: Maybe Seconds 64 | ,max_messages :: Maybe Int 65 | ,color :: ColorMode 66 | ,setup :: [String] 67 | ,allow_eval :: Bool 68 | ,target :: [String] 69 | } 70 | deriving (Data,Typeable,Show) 71 | 72 | -- | When to colour terminal output. 73 | data ColorMode 74 | = Never -- ^ Terminal output will never be coloured. 75 | | Always -- ^ Terminal output will always be coloured. 76 | | Auto -- ^ Terminal output will be coloured if $TERM and stdout appear to support it. 77 | deriving (Show, Typeable, Data) 78 | 79 | options :: Mode (CmdArgs Options) 80 | options = cmdArgsMode $ Options 81 | {command = "" &= name "c" &= typ "COMMAND" &= help "Command to run (defaults to ghci or cabal repl)" 82 | ,arguments = [] &= args &= typ "MODULE" 83 | ,test = [] &= name "T" &= typ "EXPR" &= help "Command to run after successful loading" 84 | ,test_message = "Running test..." &= typ "MESSAGE" &= help "Message to show before running the test (defaults to \"Running test...\")" 85 | ,run = [] &= name "r" &= typ "EXPR" &= opt "main" &= help "Command to run after successful loading (like --test but defaults to main)" 86 | ,warnings = False &= name "W" &= help "Allow tests to run even with warnings" 87 | ,lint = Nothing &= typ "COMMAND" &= name "lint" &= opt "hlint" &= help "Linter to run if there are no errors. Defaults to hlint." 88 | ,no_status = False &= name "S" &= help "Suppress status messages" 89 | ,clear = False &= name "clear" &= help "Clear screen when reloading" 90 | ,reverse_errors = False &= help "Reverse output order (works best with --no-height-limit)" 91 | ,no_height_limit = False &= name "no-height-limit" &= help "Disable height limit" 92 | ,height = Nothing &= help "Number of lines to use (defaults to console height)" 93 | ,width = Nothing &= name "w" &= help "Number of columns to use (defaults to console width)" 94 | ,topmost = False &= name "t" &= help "Set window topmost (Windows only)" 95 | ,no_title = False &= help "Don't update the shell title/icon" 96 | ,project = "" &= typ "NAME" &= help "Name of the project, defaults to current directory" 97 | ,restart = [] &= typ "PATH" &= help "Restart the command when the given file or directory contents change (defaults to .ghci and any .cabal file, unless when using stack or a custom command)" 98 | ,reload = [] &= typ "PATH" &= help "Reload when the given file or directory contents change (defaults to none)" 99 | ,directory = "." &= typDir &= name "C" &= help "Set the current directory" 100 | ,outputfile = [] &= typFile &= name "o" &= help "File to write the full output to" 101 | ,ignoreLoaded = False &= explicit &= name "ignore-loaded" &= help "Keep going if no files are loaded. Requires --reload to be set." 102 | ,poll = Nothing &= typ "SECONDS" &= opt "0.1" &= explicit &= name "poll" &= help "Use polling every N seconds (defaults to using notifiers)" 103 | ,max_messages = Nothing &= name "n" &= help "Maximum number of messages to print" 104 | ,color = Auto &= name "colour" &= name "color" &= opt Always &= typ "always/never/auto" &= help "Color output (defaults to when the terminal supports it)" 105 | ,setup = [] &= name "setup" &= typ "COMMAND" &= help "Setup commands to pass to ghci on stdin, usually :set " 106 | ,allow_eval = False &= name "allow-eval" &= help "Execute REPL commands in comments" 107 | ,target = [] &= typ "TARGET" &= help "Target Component to build (e.g. lib:foo for Cabal, foo:lib for Stack)" 108 | } &= verbosity &= 109 | program "ghcid" &= summary ("Auto reloading GHCi daemon v" ++ showVersion version) 110 | 111 | 112 | {- 113 | What happens on various command lines: 114 | 115 | Hlint with no .ghci file: 116 | - cabal repl - prompt with Language.Haskell.HLint loaded 117 | - cabal exec ghci Sample.hs - prompt with Sample.hs loaded 118 | - ghci - prompt with nothing loaded 119 | - ghci Sample.hs - prompt with Sample.hs loaded 120 | - stack ghci - prompt with all libraries and Main loaded 121 | 122 | Hlint with a .ghci file: 123 | - cabal repl - loads everything twice, prompt with Language.Haskell.HLint loaded 124 | - cabal exec ghci Sample.hs - loads everything first, then prompt with Sample.hs loaded 125 | - ghci - prompt with everything 126 | - ghci Sample.hs - loads everything first, then prompt with Sample.hs loaded 127 | - stack ghci - loads everything first, then prompt with libraries and Main loaded 128 | 129 | Warnings: 130 | - cabal repl won't pull in any C files (e.g. hoogle) 131 | - cabal exec ghci won't work with modules that import an autogen Paths module 132 | 133 | As a result, we prefer to give users full control with a .ghci file, if available 134 | -} 135 | autoOptions :: Options -> IO Options 136 | autoOptions o@Options{..} 137 | | command /= "" = pure $ f [command] [] 138 | | otherwise = do 139 | curdir <- getCurrentDirectory 140 | files <- getDirectoryContents "." 141 | 142 | -- use unsafePerformIO to get nicer pattern matching for logic (read-only operations) 143 | let findStack dir = flip catchIOError (const $ pure Nothing) $ do 144 | let yaml = dir "stack.yaml" 145 | b <- doesFileExist yaml &&^ doesDirectoryExist (dir ".stack-work") 146 | pure $ if b then Just yaml else Nothing 147 | stackFile <- firstJustM findStack [".",".."] -- stack file might be parent, see #62 148 | 149 | let cabal = map (curdir ) $ filter ((==) ".cabal" . takeExtension) files 150 | let isLib = isPrefixOf "lib:" -- `lib:foo` is the Cabal format 151 | let noCode = [ "-fno-code" 152 | | null test 153 | && null run 154 | && not allow_eval 155 | && (isJust stackFile || all isLib target) ] 156 | let opts = noCode ++ ghciFlagsRequired ++ ghciFlagsUseful 157 | pure $ case () of 158 | _ | Just stack <- stackFile -> 159 | let flags = if null arguments then 160 | "stack ghci" : target ++ "--test --bench" : 161 | ["--no-load" | ".ghci" `elem` files] ++ 162 | map ("--ghci-options=" ++) opts 163 | else 164 | "stack exec --test --bench -- ghci" : opts 165 | in f flags $ stack:cabal 166 | | ".ghci" `elem` files -> f ("ghci":opts) [curdir ".ghci"] 167 | | cabal /= [] -> 168 | let useCabal = ["cabal","repl"] ++ target ++ map ("--repl-options=" ++) opts 169 | useGhci = "cabal exec -- ghci":opts 170 | in f (if null arguments then useCabal else useGhci) cabal 171 | | otherwise -> f ("ghci":opts) [] 172 | where 173 | f c r = o{command = unwords $ c ++ map escape arguments, arguments = [], restart = restart ++ r, run = [], test = run ++ test} 174 | 175 | -- | Simple escaping for command line arguments. Wraps a string in double quotes if it contains a space. 176 | escape x | ' ' `elem` x = "\"" ++ x ++ "\"" 177 | | otherwise = x 178 | 179 | -- | Use arguments from .ghcid if present 180 | withGhcidArgs :: IO a -> IO a 181 | withGhcidArgs act = do 182 | b <- doesFileExist ".ghcid" 183 | if not b then act else do 184 | extra <- concatMap splitArgs . lines <$> readFile' ".ghcid" 185 | orig <- getArgs 186 | withArgs (extra ++ orig) act 187 | 188 | 189 | data TermSize = TermSize 190 | {termWidth :: Int 191 | ,termHeight :: Maybe Int -- ^ Nothing means the height is unlimited 192 | ,termWrap :: WordWrap 193 | } 194 | 195 | -- | On the 'UnexpectedExit' exception exit with a nice error message. 196 | handleErrors :: IO () -> IO () 197 | handleErrors = handle $ \(UnexpectedExit cmd _ mmsg) -> do 198 | putStr $ "Command \"" ++ cmd ++ "\" exited unexpectedly" 199 | putStrLn $ case mmsg of 200 | Just msg -> " with error message: " ++ msg 201 | Nothing -> "" 202 | exitFailure 203 | 204 | printStopped :: Options -> IO () 205 | printStopped opts = 206 | forM_ (outputfile opts) $ \file -> do 207 | writeFile file "Ghcid has stopped.\n" 208 | 209 | 210 | -- | Like 'main', but run with a fake terminal for testing 211 | mainWithTerminal :: IO TermSize -> ([String] -> IO ()) -> IO () 212 | mainWithTerminal termSize termOutput = do 213 | opts <- withGhcidArgs $ cmdArgsRun options 214 | whenLoud $ do 215 | outStrLn $ "%OS: " ++ os 216 | outStrLn $ "%ARCH: " ++ arch 217 | outStrLn $ "%VERSION: " ++ showVersion version 218 | args <- getArgs 219 | outStrLn $ "%ARGUMENTS: " ++ show args 220 | flip finally (printStopped opts) $ handleErrors $ 221 | forever $ withWindowIcon $ withSession $ \session -> do 222 | setVerbosity Normal -- undo any --verbose flags 223 | 224 | -- On certain Cygwin terminals stdout defaults to BlockBuffering 225 | hSetBuffering stdout LineBuffering 226 | hSetBuffering stderr NoBuffering 227 | origDir <- getCurrentDirectory 228 | withCurrentDirectory (directory opts) $ do 229 | opts <- autoOptions opts 230 | opts <- pure $ opts{restart = nubOrd $ (origDir ".ghcid") : restart opts, reload = nubOrd $ reload opts} 231 | when (topmost opts) terminalTopmost 232 | 233 | let noHeight = if no_height_limit opts then const Nothing else id 234 | termSize <- pure $ case (width opts, height opts) of 235 | (Just w, Just h) -> pure $ TermSize w (noHeight $ Just h) WrapHard 236 | (w, h) -> do 237 | term <- termSize 238 | -- if we write to the final column of the window then it wraps automatically 239 | -- so putStrLn width 'x' uses up two lines 240 | pure $ TermSize 241 | (fromMaybe (pred $ termWidth term) w) 242 | (noHeight $ h <|> termHeight term) 243 | (if isJust w then WrapHard else termWrap term) 244 | 245 | restyle <- do 246 | useStyle <- case color opts of 247 | Always -> pure True 248 | Never -> pure False 249 | Auto -> hSupportsANSI stdout 250 | when useStyle $ do 251 | h <- lookupEnv "HSPEC_OPTIONS" 252 | when (isNothing h) $ setEnv "HSPEC_OPTIONS" "--color" -- see #87 253 | pure $ if useStyle then id else map unescape 254 | 255 | clear <- pure $ 256 | if clear opts 257 | then (clearScreen *>) 258 | else id 259 | 260 | maybe withWaiterNotify withWaiterPoll (poll opts) $ \waiter -> 261 | runGhcid (if allow_eval opts then enableEval session else session) waiter termSize (clear . termOutput . restyle) opts 262 | 263 | 264 | 265 | main :: IO () 266 | main = mainWithTerminal termSize termOutput 267 | where 268 | termSize = do 269 | x <- Term.size 270 | pure $ case x of 271 | Nothing -> TermSize 80 (Just 8) WrapHard 272 | Just t -> TermSize (Term.width t) (Just $ Term.height t) WrapSoft 273 | 274 | termOutput xs = do 275 | outStr $ concatMap ('\n':) xs 276 | hFlush stdout -- must flush, since we don't finish with a newline 277 | 278 | 279 | data Continue = Continue 280 | 281 | data ReloadMode = Reload | Restart deriving (Show, Ord, Eq) 282 | 283 | -- If we return successfully, we restart the whole process 284 | -- Use Continue not () so that inadvertant exits don't restart 285 | runGhcid :: Session -> Waiter -> IO TermSize -> ([String] -> IO ()) -> Options -> IO Continue 286 | runGhcid session waiter termSize termOutput opts@Options{..} = do 287 | let limitMessages = maybe id (take . max 1) max_messages 288 | 289 | let outputFill :: String -> Maybe (Int, [Load]) -> [EvalResult] -> [String] -> IO () 290 | outputFill currTime load evals msg = do 291 | load <- pure $ case load of 292 | Nothing -> [] 293 | Just (loadedCount, msgs) -> prettyOutput currTime loadedCount (filter isMessage msgs) evals 294 | TermSize{..} <- termSize 295 | let wrap = concatMap (wordWrapE termWidth (termWidth `div` 5) . Esc) 296 | (msg, load, pad) <- 297 | case termHeight of 298 | Nothing -> pure (wrap msg, wrap load, []) 299 | Just termHeight -> do 300 | (termHeight, msg) <- pure $ takeRemainder termHeight $ wrap msg 301 | (termHeight, load) <- 302 | let takeRemainder' = 303 | if reverse_errors 304 | then -- When reversing the errors we want to crop out 305 | -- the top instead of the bottom of the load 306 | fmap reverse . takeRemainder termHeight . reverse 307 | else takeRemainder termHeight 308 | in pure $ takeRemainder' $ wrap load 309 | pure (msg, load, replicate termHeight "") 310 | let mergeSoft ((Esc x,WrapSoft):(Esc y,q):xs) = mergeSoft $ (Esc (x++y), q) : xs 311 | mergeSoft ((x,_):xs) = x : mergeSoft xs 312 | mergeSoft [] = [] 313 | 314 | applyPadding x = 315 | if reverse_errors 316 | then pad ++ x 317 | else x ++ pad 318 | termOutput $ applyPadding $ map fromEsc ((if termWrap == WrapSoft then mergeSoft else map fst) $ load ++ msg) 319 | 320 | when (ignoreLoaded && null reload) $ do 321 | putStrLn "--reload must be set when using --ignore-loaded" 322 | exitFailure 323 | 324 | nextWait <- waitFiles waiter 325 | (messages, loaded) <- sessionStart session command $ 326 | map (":set " ++) (ghciFlagsUseful ++ ghciFlagsUsefulVersioned) ++ setup 327 | 328 | when (null loaded && not ignoreLoaded) $ do 329 | putStrLn $ "\nNo files loaded, meaning ghcid will never refresh, so aborting.\nCommand: " ++ command 330 | exitFailure 331 | 332 | restart <- pure $ nubOrd $ restart ++ [x | LoadConfig x <- messages] 333 | -- Note that we capture restarting items at this point, not before invoking the command 334 | -- The reason is some restart items may be generated by the command itself 335 | restartTimes <- mapM getModTime restart 336 | 337 | project <- if project /= "" then pure project else takeFileName <$> getCurrentDirectory 338 | 339 | -- fire, given a waiter, the messages/loaded/touched 340 | let 341 | fire 342 | :: ([(FilePath, ReloadMode)] -> IO (Either String [(FilePath, ReloadMode)])) 343 | -> ([Load], [FilePath], [FilePath]) 344 | -> IO Continue 345 | fire nextWait (messages, loaded, touched) = do 346 | currTime <- getShortTime 347 | let loadedCount = length loaded 348 | whenLoud $ do 349 | outStrLn $ "%MESSAGES: " ++ show messages 350 | outStrLn $ "%LOADED: " ++ show loaded 351 | 352 | let evals = [e | Eval e <- messages] 353 | let (countErrors, countWarnings) = both sum $ unzip 354 | [if loadSeverity == Error then (1,0) else (0,1) | m@Message{..} <- messages, loadMessage /= []] 355 | let hasErrors = countErrors /= 0 || (countWarnings /= 0 && not warnings) 356 | test <- pure $ 357 | if null test || hasErrors then Nothing 358 | else Just $ intercalate "\n" test 359 | 360 | unless no_title $ setWindowIcon $ 361 | if countErrors > 0 then IconError else if countWarnings > 0 then IconWarning else IconOK 362 | 363 | let updateTitle extra = unless no_title $ setTitle $ unescape $ 364 | let f n msg = if n == 0 then "" else show n ++ " " ++ msg ++ ['s' | n > 1] 365 | in (if countErrors == 0 && countWarnings == 0 then allGoodMessage ++ ", at " ++ currTime else f countErrors "error" ++ 366 | (if countErrors > 0 && countWarnings > 0 then ", " else "") ++ f countWarnings "warning") ++ 367 | " " ++ extra ++ [' ' | extra /= ""] ++ "- " ++ project 368 | 369 | updateTitle $ if isJust test then "(running test)" else "" 370 | 371 | -- order and restrict the messages 372 | -- nubOrdOn loadMessage because module cycles generate the same message at several different locations 373 | ordMessages <- do 374 | let (msgError, msgWarn) = partition ((==) Error . loadSeverity) $ nubOrdOn loadMessage $ filter isMessage messages 375 | -- sort error messages by modtime, so newer edits cause the errors to float to the top - see #153 376 | errTimes <- sequence [(x,) <$> getModTime x | x <- nubOrd $ map loadFile msgError] 377 | let f x = lookup (loadFile x) errTimes 378 | moduleSorted = sortOn (Down . f) msgError ++ msgWarn 379 | pure $ (if reverse_errors then reverse else id) moduleSorted 380 | 381 | outputFill currTime (Just (loadedCount, ordMessages)) evals [test_message | isJust test] 382 | forM_ outputfile $ \file -> 383 | writeFile file $ 384 | if takeExtension file == ".json" then 385 | showJSON [("loaded",map jString loaded),("messages",map jMessage $ filter isMessage messages)] 386 | else 387 | unlines $ map unescape $ prettyOutput currTime loadedCount (limitMessages ordMessages) evals 388 | when (null loaded && not ignoreLoaded) $ do 389 | putStrLn "No files loaded, nothing to wait for. Fix the last error and restart." 390 | exitFailure 391 | whenJust test $ \t -> do 392 | whenLoud $ outStrLn $ "%TESTING: " ++ t 393 | sessionExecAsync session t $ \stderr -> do 394 | whenLoud $ outStrLn "%TESTING: Completed" 395 | hFlush stdout -- may not have been a terminating newline from test output 396 | if "*** Exception: " `isPrefixOf` stderr then do 397 | updateTitle "(test failed)" 398 | setWindowIcon IconError 399 | else do 400 | updateTitle "(test done)" 401 | whenNormal $ outStrLn "\n...done" 402 | whenJust lint $ \lintcmd -> 403 | unless hasErrors $ do 404 | (exitcode, stdout, stderr) <- readCreateProcessWithExitCode (shell . unwords $ lintcmd : map escape touched) "" 405 | unless (exitcode == ExitSuccess) $ do 406 | let output = stdout ++ stderr 407 | outStrLn output 408 | forM_ outputfile $ flip writeFile output 409 | 410 | reason <- nextWait $ map (,Restart) restart 411 | ++ map (,Reload) reload 412 | ++ map (,Reload) loaded 413 | 414 | let reason1 = case reason of 415 | Left err -> 416 | (Reload, ["Error when waiting, if this happens repeatedly, raise a ghcid bug.", err]) 417 | Right files -> 418 | case partition (\(f, mode) -> mode == Reload) files of 419 | -- Prefer restarts over reloads. E.g., in case of both '--reload=dir' 420 | -- and '--restart=dir', ghcid would restart instead of reload. 421 | (_, rs@(_:_)) -> (Restart, map fst rs) 422 | (rl, _) -> (Reload, map fst rl) 423 | 424 | currTime <- getShortTime 425 | case reason1 of 426 | (Reload, reason2) -> do 427 | unless no_status $ outputFill currTime Nothing evals $ "Reloading..." : map (" " ++) reason2 428 | nextWait <- waitFiles waiter 429 | fire nextWait =<< sessionReload session 430 | (Restart, reason2) -> do 431 | -- exit cleanly, since the whole thing is wrapped in a forever 432 | unless no_status $ outputFill currTime Nothing evals $ "Restarting..." : map (" " ++) reason2 433 | pure Continue 434 | 435 | fire nextWait (messages, loaded, loaded) 436 | 437 | 438 | -- | Given an available height, and a set of messages to display, show them as best you can. 439 | prettyOutput :: String -> Int -> [Load] -> [EvalResult] -> [String] 440 | prettyOutput currTime loadedCount [] evals = 441 | (allGoodMessage ++ " (" ++ show loadedCount ++ " module" ++ ['s' | loadedCount /= 1] ++ ", at " ++ currTime ++ ")") 442 | : concatMap printEval evals 443 | prettyOutput _ _ xs evals = concatMap loadMessage xs ++ concatMap printEval evals 444 | 445 | printEval :: EvalResult -> [String] 446 | printEval (EvalResult file (line, col) msg result) = 447 | [ " " 448 | , concat 449 | [ file 450 | , ":" 451 | , show line 452 | , ":" 453 | , show col 454 | ] 455 | ] ++ map ("$> " ++) (lines msg) 456 | ++ lines result 457 | 458 | 459 | showJSON :: [(String, [String])] -> String 460 | showJSON xs = unlines $ concat $ 461 | [ ((if i == 0 then "{" else ",") ++ jString a ++ ":") : 462 | [" " ++ (if j == 0 then "[" else ",") ++ b | (j,b) <- zipFrom 0 bs] ++ 463 | [if null bs then " []" else " ]"] 464 | | (i,(a,bs)) <- zipFrom 0 xs] ++ 465 | [["}"]] 466 | 467 | jString x = "\"" ++ escapeJSON x ++ "\"" 468 | 469 | jMessage Message{..} = jDict $ 470 | [("severity",jString $ show loadSeverity) 471 | ,("file",jString loadFile)] ++ 472 | [("start",pair loadFilePos) | loadFilePos /= (0,0)] ++ 473 | [("end", pair loadFilePosEnd) | loadFilePos /= loadFilePosEnd] ++ 474 | [("message", jString $ intercalate "\n" loadMessage)] 475 | where pair (a,b) = "[" ++ show a ++ "," ++ show b ++ "]" 476 | 477 | jDict xs = "{" ++ intercalate ", " [jString a ++ ":" ++ b | (a,b) <- xs] ++ "}" 478 | -------------------------------------------------------------------------------- /src/Language/Haskell/Ghcid.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE RecordWildCards #-} 2 | 3 | -- | Library for spawning and working with Ghci sessions. 4 | module Language.Haskell.Ghcid( 5 | Ghci, GhciError(..), Stream(..), 6 | Load(..), Severity(..), 7 | startGhci, startGhciProcess, stopGhci, interrupt, process, 8 | execStream, showModules, showPaths, reload, exec, quit 9 | ) where 10 | 11 | import System.IO 12 | import System.IO.Error 13 | import System.Process 14 | import System.Time.Extra 15 | import Control.Concurrent.Extra 16 | import Control.Exception.Extra 17 | import Control.Monad.Extra 18 | import Data.Function 19 | import Data.List.Extra 20 | import Data.Maybe 21 | import Data.IORef 22 | import Control.Applicative 23 | import Data.Unique 24 | 25 | import System.Console.CmdArgs.Verbosity 26 | 27 | import Language.Haskell.Ghcid.Parser 28 | import Language.Haskell.Ghcid.Types as T 29 | import Language.Haskell.Ghcid.Util 30 | import Prelude 31 | 32 | 33 | -- | A GHCi session. Created with 'startGhci', closed with 'stopGhci'. 34 | -- 35 | -- The interactions with a 'Ghci' session must all occur single-threaded, 36 | -- or an error will be raised. The only exception is 'interrupt', which aborts 37 | -- a running computation, or does nothing if no computation is running. 38 | data Ghci = Ghci 39 | {ghciProcess :: ProcessHandle 40 | ,ghciInterrupt :: IO () 41 | ,ghciExec :: String -> (Stream -> String -> IO ()) -> IO () 42 | ,ghciUnique :: Unique 43 | } 44 | 45 | instance Eq Ghci where 46 | a == b = ghciUnique a == ghciUnique b 47 | 48 | 49 | withCreateProc proc f = do 50 | let undo (_, _, _, proc) = ignored $ terminateProcess proc 51 | bracketOnError (createProcess proc) undo $ \(a,b,c,d) -> f a b c d 52 | 53 | -- | Start GHCi by running the described process, returning the result of the initial loading. 54 | -- If you do not call 'stopGhci' then the underlying process may be leaked. 55 | -- The callback will be given the messages produced while loading, useful if invoking something like "cabal repl" 56 | -- which might compile dependent packages before really loading. 57 | -- 58 | -- To create a 'CreateProcess' use the functions in "System.Process", particularly 59 | -- 'System.Process.shell' and 'System.Process.proc'. 60 | -- 61 | -- @since 0.6.11 62 | startGhciProcess :: CreateProcess -> (Stream -> String -> IO ()) -> IO (Ghci, [Load]) 63 | startGhciProcess process echo0 = do 64 | let proc = process{std_in=CreatePipe, std_out=CreatePipe, std_err=CreatePipe, create_group=True} 65 | withCreateProc proc $ \(Just inp) (Just out) (Just err) ghciProcess -> do 66 | 67 | hSetBuffering out LineBuffering 68 | hSetBuffering err LineBuffering 69 | hSetBuffering inp LineBuffering 70 | let writeInp x = do 71 | whenLoud $ outStrLn $ "%STDIN: " ++ x 72 | hPutStrLn inp x 73 | 74 | -- Some programs (e.g. stack) might use stdin before starting ghci (see #57) 75 | -- Send them an empty line 76 | hPutStrLn inp "" 77 | 78 | -- We don't use the GHCi prompt, so set it to a special string and filter that out. 79 | -- It could be removed as per https://github.com/ndmitchell/ghcid/issues/333 80 | let ghcid_prefix = "#~GHCID-START~#" 81 | let removePrefix = dropPrefixRepeatedly ghcid_prefix 82 | 83 | -- At various points I need to ensure everything the user is waiting for has completed 84 | -- So I send messages on stdout/stderr and wait for them to arrive 85 | syncCount <- newVar 0 86 | let syncReplay = do 87 | i <- readVar syncCount 88 | -- useful to avoid overloaded strings by showing the ['a','b','c'] form, see #109 89 | let showStr xs = "[" ++ intercalate "," (map show xs) ++ "]" 90 | let msg = "#~GHCID-FINISH-" ++ show i ++ "~#" 91 | -- Prepend a leading \n to try and avoid junk already on stdout, 92 | -- e.g. https://github.com/ndmitchell/ghcid/issues/291 93 | writeInp $ "\nINTERNAL_GHCID.putStrLn " ++ showStr msg ++ "\n" ++ 94 | "INTERNAL_GHCID.hPutStrLn INTERNAL_GHCID.stderr " ++ showStr msg 95 | pure $ isInfixOf msg 96 | let syncFresh = do 97 | modifyVar_ syncCount $ pure . succ 98 | syncReplay 99 | 100 | -- Consume from a stream until EOF (pure Nothing) or some predicate returns Just 101 | let consume :: Stream -> (String -> IO (Maybe a)) -> IO (Either (Maybe String) a) 102 | consume name finish = do 103 | let h = if name == Stdout then out else err 104 | flip fix Nothing $ \rec oldMsg -> do 105 | el <- tryBool isEOFError $ hGetLine h 106 | case el of 107 | Left _ -> pure $ Left oldMsg 108 | Right l -> do 109 | whenLoud $ outStrLn $ "%" ++ upper (show name) ++ ": " ++ l 110 | let msg = removePrefix l 111 | res <- finish msg 112 | case res of 113 | Nothing -> rec $ Just msg 114 | Just a -> pure $ Right a 115 | 116 | let consume2 :: String -> (Stream -> String -> IO (Maybe a)) -> IO (a,a) 117 | consume2 msg finish = do 118 | -- fetch the operations in different threads as hGetLine may block 119 | -- and can't be aborted by async exceptions, see #154 120 | res1 <- onceFork $ consume Stdout (finish Stdout) 121 | res2 <- onceFork $ consume Stderr (finish Stderr) 122 | res1 <- res1 123 | res2 <- res2 124 | let raise msg err = throwIO $ case cmdspec process of 125 | ShellCommand cmd -> UnexpectedExit cmd msg err 126 | RawCommand exe args -> UnexpectedExit (unwords (exe:args)) msg err 127 | case (res1, res2) of 128 | (Right v1, Right v2) -> pure (v1, v2) 129 | (_, Left err) -> raise msg err 130 | (_, Right _) -> raise msg Nothing 131 | 132 | -- held while interrupting, and briefly held when starting an exec 133 | -- ensures exec values queue up behind an ongoing interrupt and no two interrupts run at once 134 | isInterrupting <- newLock 135 | 136 | -- is anyone running running an exec statement, ensure only one person talks to ghci at a time 137 | isRunning <- newLock 138 | 139 | let ghciExec command echo = do 140 | withLock isInterrupting $ pure () 141 | res <- withLockTry isRunning $ do 142 | writeInp command 143 | stop <- syncFresh 144 | void $ consume2 command $ \strm s -> 145 | if stop s then pure $ Just () else do echo strm s; pure Nothing 146 | when (isNothing res) $ 147 | fail "Ghcid.exec, computation is already running, must be used single-threaded" 148 | 149 | let ghciInterrupt = withLock isInterrupting $ 150 | whenM (fmap isNothing $ withLockTry isRunning $ pure ()) $ do 151 | whenLoud $ outStrLn "%INTERRUPT" 152 | interruptProcessGroupOf ghciProcess 153 | -- let the person running ghciExec finish, since their sync messages 154 | -- may have been the ones that got interrupted 155 | syncReplay 156 | -- now wait for the person doing ghciExec to have actually left the lock 157 | withLock isRunning $ pure () 158 | -- there may have been two syncs sent, so now do a fresh sync to clear everything 159 | stop <- syncFresh 160 | void $ consume2 "Interrupt" $ \_ s -> pure $ if stop s then Just () else Nothing 161 | 162 | ghciUnique <- newUnique 163 | let ghci = Ghci{..} 164 | 165 | -- Now wait for 'GHCi, version' to appear before sending anything real, required for #57 166 | stdout <- newIORef [] 167 | stderr <- newIORef [] 168 | sync <- newIORef $ const False 169 | consume2 "" $ \strm s -> do 170 | stop <- readIORef sync 171 | if stop s then 172 | pure $ Just () 173 | else do 174 | -- there may be some initial prompts on stdout before I set the prompt properly 175 | s <- pure $ maybe s (removePrefix . snd) $ stripInfix ghcid_prefix s 176 | whenLoud $ outStrLn $ "%STDOUT2: " ++ s 177 | modifyIORef (if strm == Stdout then stdout else stderr) (s:) 178 | when (any (`isPrefixOf` s) [ "GHCi, version " 179 | , "GHCJSi, version " 180 | , "Clashi, version " ]) $ do 181 | -- the thing before me may have done its own Haskell compiling 182 | writeIORef stdout [] 183 | writeIORef stderr [] 184 | writeInp "import qualified System.IO as INTERNAL_GHCID" 185 | writeInp ":unset +t +s" -- see https://github.com/ndmitchell/ghcid/issues/162 186 | writeInp $ ":set prompt " ++ ghcid_prefix 187 | writeInp $ ":set prompt-cont " ++ ghcid_prefix 188 | 189 | -- failure isn't harmful, so do them one-by-one 190 | forM_ (ghciFlagsRequired ++ ghciFlagsRequiredVersioned) $ \flag -> 191 | writeInp $ ":set " ++ flag 192 | writeIORef sync =<< syncFresh 193 | echo0 strm s 194 | pure Nothing 195 | r1 <- parseLoad . reverse <$> ((++) <$> readIORef stderr <*> readIORef stdout) 196 | -- see #132, if hide-source-paths was turned on the modules didn't get printed out properly 197 | -- so try a showModules to capture the information again 198 | r2 <- if any isLoading r1 then pure [] else map (uncurry Loading) <$> showModules ghci 199 | execStream ghci "" echo0 200 | pure (ghci, r1 ++ r2) 201 | 202 | 203 | -- | Start GHCi by running the given shell command, a helper around 'startGhciProcess'. 204 | startGhci 205 | :: String -- ^ Shell command 206 | -> Maybe FilePath -- ^ Working directory 207 | -> (Stream -> String -> IO ()) -- ^ Output callback 208 | -> IO (Ghci, [Load]) 209 | startGhci cmd directory = startGhciProcess (shell cmd){cwd=directory} 210 | 211 | 212 | -- | Execute a command, calling a callback on each response. 213 | -- The callback will be called single threaded. 214 | execStream :: Ghci -> String -> (Stream -> String -> IO ()) -> IO () 215 | execStream = ghciExec 216 | 217 | -- | Interrupt Ghci, stopping the current computation (if any), 218 | -- but leaving the process open to new input. 219 | interrupt :: Ghci -> IO () 220 | interrupt = ghciInterrupt 221 | 222 | -- | Obtain the progress handle behind a GHCi instance. 223 | process :: Ghci -> ProcessHandle 224 | process = ghciProcess 225 | 226 | 227 | --------------------------------------------------------------------- 228 | -- SUGAR HELPERS 229 | 230 | -- | Execute a command, calling a callback on each response. 231 | -- The callback will be called single threaded. 232 | execBuffer :: Ghci -> String -> (Stream -> String -> IO ()) -> IO [String] 233 | execBuffer ghci cmd echo = do 234 | stdout <- newIORef [] 235 | stderr <- newIORef [] 236 | execStream ghci cmd $ \strm s -> do 237 | modifyIORef (if strm == Stdout then stdout else stderr) (s:) 238 | echo strm s 239 | reverse <$> ((++) <$> readIORef stderr <*> readIORef stdout) 240 | 241 | -- | Send a command, get lines of result. Must be called single-threaded. 242 | exec :: Ghci -> String -> IO [String] 243 | exec ghci cmd = execBuffer ghci cmd $ \_ _ -> pure () 244 | 245 | -- | List the modules currently loaded, with module name and source file. 246 | showModules :: Ghci -> IO [(String,FilePath)] 247 | showModules ghci = parseShowModules <$> exec ghci ":show modules" 248 | 249 | -- | Return the current working directory, and a list of module import paths 250 | showPaths :: Ghci -> IO (FilePath, [FilePath]) 251 | showPaths ghci = parseShowPaths <$> exec ghci ":show paths" 252 | 253 | -- | Perform a reload, list the messages that reload generated. 254 | reload :: Ghci -> IO [Load] 255 | reload ghci = parseLoad <$> exec ghci ":reload" 256 | 257 | -- | Send @:quit@ and wait for the process to quit. 258 | quit :: Ghci -> IO () 259 | quit ghci = do 260 | interrupt ghci 261 | handle (\UnexpectedExit{} -> pure ()) $ void $ exec ghci ":quit" 262 | -- Be aware that waitForProcess has a race condition, see https://github.com/haskell/process/issues/46. 263 | -- Therefore just ignore the exception anyway, its probably already terminated. 264 | ignored $ void $ waitForProcess $ process ghci 265 | 266 | 267 | -- | Stop GHCi. Attempts to interrupt and execute @:quit:@, but if that doesn't complete 268 | -- within 5 seconds it just terminates the process. 269 | stopGhci :: Ghci -> IO () 270 | stopGhci ghci = do 271 | forkIO $ do 272 | -- if nicely doesn't work, kill ghci as the process level 273 | sleep 5 274 | terminateProcess $ process ghci 275 | quit ghci 276 | -------------------------------------------------------------------------------- /src/Language/Haskell/Ghcid/Escape.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE PatternGuards #-} 2 | 3 | -- | Module for dealing with escape codes 4 | module Language.Haskell.Ghcid.Escape( 5 | WordWrap(..), 6 | Esc(..), unescape, 7 | stripInfixE, stripPrefixE, isPrefixOfE, spanE, trimStartE, unwordsE, unescapeE, 8 | wordWrapE 9 | ) where 10 | 11 | import Data.Char 12 | import Data.Either.Extra 13 | import Data.List.Extra 14 | import Data.Maybe 15 | import Data.Tuple.Extra 16 | import Control.Applicative 17 | import Prelude 18 | 19 | 20 | -- A string with escape characters in it 21 | newtype Esc = Esc {fromEsc :: String} 22 | deriving (Eq,Show) 23 | 24 | app (Esc x) (Esc y) = Esc $ x ++ y 25 | 26 | unesc :: Esc -> Maybe (Either Esc Char, Esc) 27 | unesc (Esc ('\ESC':xs)) | (pre,'m':post) <- break (== 'm') xs = Just (Left $ Esc $ '\ESC':pre++"m", Esc post) 28 | unesc (Esc (x:xs)) = Just (Right x, Esc xs) 29 | unesc (Esc []) = Nothing 30 | 31 | explode :: Esc -> [Either Esc Char] 32 | explode = unfoldr unesc 33 | 34 | implode :: [Either Esc Char] -> Esc 35 | implode = Esc . concatMap (either fromEsc pure) 36 | 37 | unescape :: String -> String 38 | unescape = unescapeE . Esc 39 | 40 | -- | Remove all escape characters in a string 41 | unescapeE :: Esc -> String 42 | unescapeE = rights . explode 43 | 44 | stripPrefixE :: String -> Esc -> Maybe Esc 45 | stripPrefixE [] e = Just e 46 | stripPrefixE (x:xs) e = case unesc e of 47 | Just (Left code, rest) -> app code <$> stripPrefixE (x:xs) rest 48 | Just (Right y, rest) | y == x -> stripPrefixE xs rest 49 | _ -> Nothing 50 | 51 | stripInfixE :: String -> Esc -> Maybe (Esc, Esc) 52 | stripInfixE needle haystack | Just rest <- stripPrefixE needle haystack = Just (Esc [], rest) 53 | stripInfixE needle e = case unesc e of 54 | Nothing -> Nothing 55 | Just (x,xs) -> first (app $ fromEither $ fmap (Esc . pure) x) <$> stripInfixE needle xs 56 | 57 | 58 | spanE, breakE :: (Char -> Bool) -> Esc -> (Esc, Esc) 59 | breakE f = spanE (not . f) 60 | spanE f e = case unesc e of 61 | Nothing -> (Esc "", Esc "") 62 | Just (Left e, rest) -> first (app e) $ spanE f rest 63 | Just (Right c, rest) | f c -> first (app $ Esc [c]) $ spanE f rest 64 | | otherwise -> (Esc "", e) 65 | 66 | isPrefixOfE :: String -> Esc -> Bool 67 | isPrefixOfE x y = isJust $ stripPrefixE x y 68 | 69 | trimStartE :: Esc -> Esc 70 | trimStartE e = case unesc e of 71 | Nothing -> Esc "" 72 | Just (Left code, rest) -> app code $ trimStartE rest 73 | Just (Right c, rest) | isSpace c -> trimStartE rest 74 | | otherwise -> e 75 | 76 | unwordsE :: [Esc] -> Esc 77 | unwordsE = Esc . unwords . map fromEsc 78 | 79 | 80 | repeatedlyE :: (Esc -> (b, Esc)) -> Esc -> [b] 81 | repeatedlyE f (Esc []) = [] 82 | repeatedlyE f as = b : repeatedlyE f as' 83 | where (b, as') = f as 84 | 85 | splitAtE :: Int -> Esc -> (Esc, Esc) 86 | splitAtE i e = case unesc e of 87 | _ | i <= 0 -> (Esc "", e) 88 | Nothing -> (e, e) 89 | Just (Left code, rest) -> first (app code) $ splitAtE i rest 90 | Just (Right c, rest) -> first (app $ Esc [c]) $ splitAtE (i-1) rest 91 | 92 | reverseE :: Esc -> Esc 93 | reverseE = implode . reverse . explode 94 | 95 | breakEndE :: (Char -> Bool) -> Esc -> (Esc, Esc) 96 | breakEndE f = swap . both reverseE . breakE f . reverseE 97 | 98 | 99 | lengthE :: Esc -> Int 100 | lengthE = length . unescapeE 101 | 102 | 103 | -- | 'WrapHard' means you have to 104 | data WordWrap = WrapHard | WrapSoft 105 | deriving (Eq,Show) 106 | 107 | 108 | -- | Word wrap a string into N separate strings. 109 | -- Flows onto a subsequent line if less than N characters end up being empty. 110 | wordWrapE :: Int -> Int -> Esc -> [(Esc, WordWrap)] 111 | wordWrapE mx gap = repeatedlyE f 112 | where 113 | f x = 114 | let (a,b) = splitAtE mx x in 115 | if b == Esc "" then ((a, WrapHard), Esc "") else 116 | let (a1,a2) = breakEndE isSpace a in 117 | if lengthE a2 <= gap then ((a1, WrapHard), app a2 b) else ((a, WrapSoft), trimStartE b) 118 | -------------------------------------------------------------------------------- /src/Language/Haskell/Ghcid/Parser.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE PatternGuards, ViewPatterns, TupleSections #-} 2 | 3 | -- | Parses the output from GHCi 4 | module Language.Haskell.Ghcid.Parser( 5 | parseShowModules, parseShowPaths, parseLoad 6 | ) where 7 | 8 | import System.FilePath 9 | import Data.Char 10 | import Data.List.Extra 11 | import Data.Maybe 12 | import Text.Read 13 | import Data.Tuple.Extra 14 | import Control.Applicative 15 | import Prelude 16 | 17 | import Language.Haskell.Ghcid.Types 18 | import Language.Haskell.Ghcid.Escape 19 | 20 | 21 | -- | Parse messages from show modules command. Given the parsed lines 22 | -- return a list of (module name, file). 23 | parseShowModules :: [String] -> [(String, FilePath)] 24 | parseShowModules (map unescape -> xs) = 25 | -- we only return raw values, don't want any escape codes in there 26 | [ (takeWhile (not . isSpace) $ trimStart a, takeWhile (/= ',') b) 27 | | x <- xs, (a,'(':' ':b) <- [break (== '(') x]] 28 | 29 | -- | Parse messages from show paths command. Given the parsed lines 30 | -- return (current working directory, module import search paths) 31 | parseShowPaths :: [String] -> (FilePath, [FilePath]) 32 | parseShowPaths (map unescape -> xs) 33 | | (_:x:_:is) <- xs = (trimStart x, map trimStart is) 34 | | otherwise = (".",[]) 35 | 36 | -- | Parse messages given on reload. 37 | parseLoad :: [String] -> [Load] 38 | -- nub, because cabal repl sometimes does two reloads at the start 39 | parseLoad (map Esc -> xs) = nubOrd $ f xs 40 | where 41 | f :: [Esc] -> [Load] 42 | 43 | -- [1 of 2] Compiling GHCi ( GHCi.hs, interpreted ) 44 | f (xs:rest) 45 | | Just xs <- stripPrefixE "[" xs 46 | = map (uncurry Loading) (parseShowModules [drop 11 $ dropWhile (/= ']') $ unescapeE xs]) ++ 47 | f rest 48 | 49 | -- GHCi.hs:81:1: Warning: Defined but not used: `foo' 50 | f (x:xs) 51 | | not $ " " `isPrefixOfE` x 52 | , Just (file,rest) <- breakFileColon x 53 | -- take position, including span if present 54 | , Just ((pos1, pos2), rest) <- parsePosition rest 55 | , (msg,las) <- span isMessageBody xs 56 | , rest <- trimStartE $ unwordsE $ rest : xs 57 | , sev <- if "warning:" `isPrefixOf` lower (unescapeE rest) then Warning else Error 58 | = Message sev file pos1 pos2 (map fromEsc $ x:msg) : f las 59 | 60 | -- : can't find file: FILENAME 61 | f (x:xs) 62 | | Just file <- stripPrefixE ": can't find file: " x 63 | = Message Error (unescapeE file) (0,0) (0,0) [fromEsc x] : f xs 64 | 65 | -- : error: 66 | f (x:xs) 67 | | ": error:" `isPrefixOfE` x 68 | , (xs,rest) <- span leadingWhitespaceE xs 69 | = Message Error "" (0,0) (0,0) (map fromEsc $ x:xs) : f rest 70 | 71 | -- Module imports form a cycle: 72 | -- module `Module' (Module.hs) imports itself 73 | f (x:xs) 74 | | unescapeE x == "Module imports form a cycle:" 75 | , (xs,rest) <- span leadingWhitespaceE xs 76 | , let ms = [takeWhile (/= ')') x | x <- xs, '(':x <- [dropWhile (/= '(') $ unescapeE x]] 77 | = [Message Error m (0,0) (0,0) (map fromEsc $ x:xs) | m <- nubOrd ms] ++ f rest 78 | 79 | -- Loaded GHCi configuration from C:\Neil\ghcid\.ghci 80 | f (x:xs) 81 | | Just x <- stripPrefixE "Loaded GHCi configuration from " x 82 | = LoadConfig (unescapeE x) : f xs 83 | 84 | f (_:xs) = f xs 85 | f [] = [] 86 | 87 | leadingWhitespaceE :: Esc -> Bool 88 | leadingWhitespaceE x = 89 | isPrefixOfE " " x || isPrefixOfE "\t" x 90 | 91 | -- 1:2: 92 | -- 1:2-4: 93 | -- (1,2)-(3,4): 94 | parsePosition :: Esc -> Maybe (((Int, Int), (Int, Int)), Esc) 95 | parsePosition x 96 | | Just (l1, x) <- digit x, Just x <- lit ":" x, Just (c1, x) <- digit x = case () of 97 | _ | Just x <- lit ":" x -> Just (((l1,c1),(l1,c1)), x) 98 | | Just x <- lit "-" x, Just (c2,x) <- digit x, Just x <- lit ":" x -> Just (((l1,c1),(l1,c2)), x) 99 | | otherwise -> Nothing 100 | | Just (p1, x) <- digits x, Just x <- lit "-" x, Just (p2, x) <- digits x, Just x <- lit ":" x = Just ((p1,p2),x) 101 | | otherwise = Nothing 102 | where 103 | lit = stripPrefixE 104 | 105 | digit x = (,b) <$> readMaybe (unescapeE a) 106 | where (a,b) = spanE isDigit x 107 | 108 | digits x = do 109 | x <- lit "(" x 110 | (l,x) <- digit x 111 | x <- lit "," x 112 | (c,x) <- digit x 113 | x <- lit ")" x 114 | pure ((l,c),x) 115 | 116 | 117 | -- After the file location, message bodies are indented (perhaps prefixed by a line number) 118 | isMessageBody :: Esc -> Bool 119 | isMessageBody xs = isPrefixOfE " " xs || case stripInfixE "|" xs of 120 | Just (prefix, _) | all (\x -> isSpace x || isDigit x) $ unescapeE prefix -> True 121 | _ -> False 122 | 123 | -- A filename, followed by a colon - be careful to handle Windows drive letters, see #61 124 | breakFileColon :: Esc -> Maybe (FilePath, Esc) 125 | breakFileColon xs = case stripInfixE ":" xs of 126 | Nothing -> Nothing 127 | Just (a,b) 128 | | [drive] <- unescapeE a, isLetter drive -> first ((++) [drive,':'] . unescapeE) <$> stripInfixE ":" b 129 | | otherwise -> Just (unescapeE a, b) 130 | -------------------------------------------------------------------------------- /src/Language/Haskell/Ghcid/Terminal.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | 3 | -- | Cross-platform operations for manipulating terminal console windows. 4 | module Language.Haskell.Ghcid.Terminal( 5 | terminalTopmost, 6 | withWindowIcon, WindowIcon(..), setWindowIcon 7 | ) where 8 | 9 | #if defined(mingw32_HOST_OS) 10 | import Data.Word 11 | import Data.Bits 12 | import Control.Exception 13 | 14 | import Graphics.Win32.Misc 15 | import Graphics.Win32.Window 16 | import Graphics.Win32.Message 17 | import Graphics.Win32.GDI.Types 18 | import System.Win32.Types 19 | 20 | 21 | wM_GETICON = 0x007F :: WindowMessage 22 | 23 | #ifdef x86_64_HOST_ARCH 24 | #define CALLCONV ccall 25 | #else 26 | #define CALLCONV stdcall 27 | #endif 28 | 29 | foreign import CALLCONV unsafe "windows.h GetConsoleWindow" 30 | getConsoleWindow :: IO HWND 31 | 32 | foreign import CALLCONV unsafe "windows.h SetWindowPos" 33 | setWindowPos :: HWND -> HWND -> Int -> Int -> Int -> Int -> Word32 -> IO Bool 34 | #endif 35 | 36 | 37 | -- | Raise the current terminal on top of all other screens, if you can. 38 | terminalTopmost :: IO () 39 | #if defined(mingw32_HOST_OS) 40 | terminalTopmost = do 41 | wnd <- getConsoleWindow 42 | setWindowPos wnd hWND_TOPMOST 0 0 0 0 (sWP_NOMOVE .|. sWP_NOSIZE) 43 | pure () 44 | #else 45 | terminalTopmost = pure () 46 | #endif 47 | 48 | 49 | data WindowIcon = IconOK | IconWarning | IconError 50 | 51 | -- | Change the window icon to green, yellow or red depending on whether the file was errorless, contained only warnings or contained at least one error. 52 | setWindowIcon :: WindowIcon -> IO () 53 | #if defined(mingw32_HOST_OS) 54 | setWindowIcon x = do 55 | ico <- pure $ case x of 56 | IconOK -> iDI_ASTERISK 57 | IconWarning -> iDI_EXCLAMATION 58 | IconError -> iDI_HAND 59 | icon <- loadIcon Nothing ico 60 | wnd <- getConsoleWindow 61 | -- SMALL is the system tray, BIG is the taskbar and Alt-Tab screen 62 | sendMessage wnd wM_SETICON iCON_SMALL $ fromIntegral $ castPtrToUINTPtr icon 63 | sendMessage wnd wM_SETICON iCON_BIG $ fromIntegral $ castPtrToUINTPtr icon 64 | pure () 65 | #else 66 | setWindowIcon _ = pure () 67 | #endif 68 | 69 | 70 | -- | Run an operation in which you call setWindowIcon 71 | withWindowIcon :: IO a -> IO a 72 | #if defined(mingw32_HOST_OS) 73 | withWindowIcon act = do 74 | wnd <- getConsoleWindow 75 | icoBig <- sendMessage wnd wM_GETICON iCON_BIG 0 76 | icoSmall <- sendMessage wnd wM_GETICON iCON_SMALL 0 77 | act `finally` do 78 | sendMessage wnd wM_SETICON iCON_BIG icoBig 79 | sendMessage wnd wM_SETICON iCON_SMALL icoSmall 80 | pure () 81 | #else 82 | withWindowIcon act = act 83 | #endif 84 | -------------------------------------------------------------------------------- /src/Language/Haskell/Ghcid/Types.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | 3 | -- | The types types that we use in Ghcid 4 | module Language.Haskell.Ghcid.Types( 5 | GhciError(..), 6 | Stream(..), 7 | Load(..), Severity(..), EvalResult(..), 8 | isMessage, isLoading, isLoadConfig 9 | ) where 10 | 11 | import Data.Data 12 | import Control.Exception.Base (Exception) 13 | 14 | -- | GHCi shut down 15 | data GhciError = UnexpectedExit 16 | {ghciErrorCmd :: String 17 | ,ghciErrorMsg :: String 18 | ,ghciErrorLastStdErr :: Maybe String 19 | } 20 | deriving (Show, Eq, Ord, Typeable, Data) 21 | 22 | -- | Make GhciError an exception 23 | instance Exception GhciError 24 | 25 | -- | The stream Ghci is talking over. 26 | data Stream = Stdout | Stderr 27 | deriving (Show,Eq,Ord,Bounded,Enum,Read,Typeable,Data) 28 | 29 | -- | Severity of messages 30 | data Severity = Warning | Error 31 | deriving (Show,Eq,Ord,Bounded,Enum,Read,Typeable,Data) 32 | 33 | -- | Load messages 34 | data Load 35 | = -- | A module/file was being loaded. 36 | Loading 37 | {loadModule :: String -- ^ The module that was being loaded, @Foo.Bar@. 38 | ,loadFile :: FilePath -- ^ The file that was being loaded, @Foo/Bar.hs@. 39 | } 40 | | -- | An error/warning was emitted. 41 | Message 42 | {loadSeverity :: Severity -- ^ The severity of the message, either 'Warning' or 'Error'. 43 | ,loadFile :: FilePath -- ^ The file the error relates to, @Foo/Bar.hs@. 44 | ,loadFilePos :: (Int,Int) -- ^ The position in the file, @(line,col)@, both 1-based. Uses @(0,0)@ for no position information. 45 | ,loadFilePosEnd :: (Int, Int) -- ^ The end position in the file, @(line,col)@, both 1-based. If not present will be the same as 'loadFilePos'. 46 | ,loadMessage :: [String] -- ^ The message, split into separate lines, may contain ANSI Escape codes. 47 | } 48 | | -- | A config file was loaded, usually a .ghci file (GHC 8.2 and above only) 49 | LoadConfig 50 | {loadFile :: FilePath -- ^ The file that was being loaded, @.ghci@. 51 | } 52 | | -- | A response to an eval comment 53 | Eval EvalResult 54 | deriving (Show, Eq, Ord) 55 | 56 | data EvalResult = EvalResult 57 | {evalFile :: FilePath -- ^ The file that was being loaded, @.ghci@. 58 | ,evalFilePos :: (Int, Int) 59 | ,evalCommand :: String 60 | ,evalResult :: String 61 | } 62 | deriving (Show, Eq, Ord) 63 | 64 | -- | Is a 'Load' a 'Message'? 65 | isMessage :: Load -> Bool 66 | isMessage Message{} = True 67 | isMessage _ = False 68 | 69 | -- | Is a 'Load' a 'Loading'? 70 | isLoading :: Load -> Bool 71 | isLoading Loading{} = True 72 | isLoading _ = False 73 | 74 | -- | Is a 'Load' a 'LoadConfig'? 75 | isLoadConfig :: Load -> Bool 76 | isLoadConfig LoadConfig{} = True 77 | isLoadConfig _ = False 78 | -------------------------------------------------------------------------------- /src/Language/Haskell/Ghcid/Util.hs: -------------------------------------------------------------------------------- 1 | 2 | -- | Utility functions 3 | module Language.Haskell.Ghcid.Util( 4 | ghciFlagsRequired, ghciFlagsRequiredVersioned, 5 | ghciFlagsUseful, ghciFlagsUsefulVersioned, 6 | dropPrefixRepeatedly, 7 | takeRemainder, 8 | outStr, outStrLn, 9 | ignored, 10 | allGoodMessage, 11 | getModTime, getModTimeResolution, getShortTime 12 | ) where 13 | 14 | import Control.Concurrent.Extra 15 | import System.Time.Extra 16 | import System.IO.Unsafe 17 | import System.IO.Extra 18 | import System.FilePath 19 | import System.Info.Extra 20 | import System.Console.ANSI 21 | import Data.Version.Extra 22 | import Data.List.Extra 23 | import Data.Time.Clock 24 | import Data.Time.Format 25 | import Data.Time.LocalTime 26 | import System.IO.Error 27 | import System.Directory 28 | import Control.Exception 29 | import Control.Monad.Extra 30 | import Control.Applicative 31 | import Prelude 32 | 33 | 34 | -- | Flags that are required for ghcid to function and are supported on all GHC versions 35 | ghciFlagsRequired :: [String] 36 | ghciFlagsRequired = 37 | ["-fno-break-on-exception","-fno-break-on-error" -- see #43 38 | ,"-v1" -- see #110 39 | ] 40 | 41 | -- | Flags that are required for ghcid to function, but are only supported on some GHC versions 42 | ghciFlagsRequiredVersioned :: [String] 43 | ghciFlagsRequiredVersioned = 44 | ["-fno-hide-source-paths" -- see #132, GHC 8.2 and above 45 | ] 46 | 47 | -- | Flags that make ghcid work better and are supported on all GHC versions 48 | ghciFlagsUseful :: [String] 49 | ghciFlagsUseful = 50 | ["-ferror-spans" -- see #148 51 | ,"-j" -- see #153, GHC 7.8 and above, but that's all I support anyway 52 | ] 53 | 54 | -- | Flags that make ghcid work better, but are only supported on some GHC versions 55 | ghciFlagsUsefulVersioned :: [String] 56 | ghciFlagsUsefulVersioned = 57 | ["-fdiagnostics-color=always" -- see #144, GHC 8.2 and above 58 | ] 59 | 60 | 61 | -- | Drop a prefix from a list, no matter how many times that prefix is present 62 | dropPrefixRepeatedly :: Eq a => [a] -> [a] -> [a] 63 | dropPrefixRepeatedly [] s = s 64 | dropPrefixRepeatedly pre s = maybe s (dropPrefixRepeatedly pre) $ stripPrefix pre s 65 | 66 | 67 | {-# NOINLINE lock #-} 68 | lock :: Lock 69 | lock = unsafePerformIO newLock 70 | 71 | -- | Output a string with some level of locking 72 | outStr :: String -> IO () 73 | outStr msg = do 74 | evaluate $ length $ show msg 75 | withLock lock $ putStr msg 76 | 77 | outStrLn :: String -> IO () 78 | outStrLn xs = outStr $ xs ++ "\n" 79 | 80 | -- | Ignore all exceptions coming from an action 81 | ignored :: IO () -> IO () 82 | ignored act = do 83 | bar <- newBarrier 84 | forkFinally act $ const $ signalBarrier bar () 85 | waitBarrier bar 86 | 87 | -- | The message to show when no errors have been reported 88 | allGoodMessage :: String 89 | allGoodMessage = setSGRCode [SetColor Foreground Dull Green] ++ "All good" ++ setSGRCode [] 90 | 91 | -- | Given a 'FilePath' return either 'Nothing' (file does not exist) or 'Just' (the modification time) 92 | getModTime :: FilePath -> IO (Maybe UTCTime) 93 | getModTime file = handleJust 94 | (\e -> if isDoesNotExistError e then Just () else Nothing) 95 | (\_ -> pure Nothing) 96 | (Just <$> getModificationTime file) 97 | 98 | -- | Returns both the amount left (could have been taken more) and the list 99 | takeRemainder :: Int -> [a] -> (Int, [a]) 100 | takeRemainder n xs = let ys = take n xs in (n - length ys, ys) 101 | 102 | -- | Get the current time in the current timezone in HH:MM:SS format 103 | getShortTime :: IO String 104 | getShortTime = formatTime defaultTimeLocale "%H:%M:%S" <$> getZonedTime 105 | 106 | 107 | -- | Get the smallest difference that can be reported by two modification times 108 | getModTimeResolution :: IO Seconds 109 | getModTimeResolution = pure getModTimeResolutionCache 110 | 111 | {-# NOINLINE getModTimeResolutionCache #-} 112 | -- Cache the result so only computed once per run 113 | getModTimeResolutionCache :: Seconds 114 | getModTimeResolutionCache = unsafePerformIO $ withTempDir $ \dir -> do 115 | let file = dir "calibrate.txt" 116 | 117 | -- with 10 measurements can get a bit slow, see Shake issue tracker #451 118 | -- if it rounds to a second then 1st will be a fraction, but 2nd will be full second 119 | mtime <- fmap maximum $ forM [1..3] $ \i -> fmap fst $ duration $ do 120 | writeFile file $ show i 121 | t1 <- getModificationTime file 122 | flip loopM 0 $ \j -> do 123 | writeFile file $ show (i,j) 124 | t2 <- getModificationTime file 125 | pure $ if t1 == t2 then Left $ j+1 else Right () 126 | 127 | -- GHC 7.6 and below only have 1 sec resolution timestamps 128 | mtime <- pure $ if compilerVersion < makeVersion [7,8] then max mtime 1 else mtime 129 | 130 | putStrLn $ "Longest file modification time lag was " ++ show (ceiling (mtime * 1000)) ++ "ms" 131 | -- add a little bit of safety, but if it's really quick, don't make it that much slower 132 | pure $ mtime + min 0.1 mtime 133 | -------------------------------------------------------------------------------- /src/Paths.hs: -------------------------------------------------------------------------------- 1 | 2 | module Paths_ghcid(version) where 3 | 4 | import Data.Version.Extra 5 | 6 | version :: Version 7 | version = makeVersion [0,0] 8 | -------------------------------------------------------------------------------- /src/Session.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE RecordWildCards #-} 2 | 3 | -- | A persistent version of the Ghci session, encoding lots of semantics on top. 4 | -- Not suitable for calling multithreaded. 5 | module Session( 6 | Session, enableEval, withSession, 7 | sessionStart, sessionReload, 8 | sessionExecAsync, 9 | ) where 10 | 11 | import Language.Haskell.Ghcid 12 | import Language.Haskell.Ghcid.Escape 13 | import Language.Haskell.Ghcid.Util 14 | import Language.Haskell.Ghcid.Types 15 | import Data.IORef 16 | import System.Console.ANSI 17 | import System.Time.Extra 18 | import System.Process 19 | import System.FilePath 20 | import Control.Exception.Extra 21 | import Control.Concurrent.Extra 22 | import Control.Monad.Extra 23 | import Data.Maybe 24 | import Data.List.Extra 25 | import Control.Applicative 26 | import Prelude 27 | import System.IO.Extra 28 | 29 | 30 | data Session = Session 31 | {ghci :: IORef (Maybe Ghci) -- ^ The Ghci session, or Nothing if there is none 32 | ,command :: IORef (Maybe (String, [String])) -- ^ The last command passed to sessionStart, setup operations 33 | ,warnings :: IORef [Load] -- ^ The warnings from the last load 34 | ,curdir :: IORef FilePath -- ^ The current working directory 35 | ,running :: Var Bool -- ^ Am I actively running an async command 36 | ,withThread :: ThreadId -- ^ Thread that called withSession 37 | ,allowEval :: Bool -- ^ Is the allow-eval flag set? 38 | } 39 | 40 | enableEval :: Session -> Session 41 | enableEval s = s { allowEval = True } 42 | 43 | 44 | debugShutdown x = when False $ print ("DEBUG SHUTDOWN", x) 45 | 46 | -- | The function 'withSession' expects to be run on the main thread, 47 | -- but the inner function will not. This ensures Ctrl-C is handled 48 | -- properly and any spawned Ghci processes will be aborted. 49 | withSession :: (Session -> IO a) -> IO a 50 | withSession f = do 51 | ghci <- newIORef Nothing 52 | command <- newIORef Nothing 53 | warnings <- newIORef [] 54 | curdir <- newIORef "." 55 | running <- newVar False 56 | debugShutdown "Starting session" 57 | withThread <- myThreadId 58 | let allowEval = False 59 | f Session{..} `finally` do 60 | debugShutdown "Start finally" 61 | modifyVar_ running $ const $ pure False 62 | whenJustM (readIORef ghci) $ \v -> do 63 | writeIORef ghci Nothing 64 | debugShutdown "Calling kill" 65 | kill v 66 | debugShutdown "Finish finally" 67 | 68 | 69 | -- | Kill. Wait just long enough to ensure you've done the job, but not to see the results. 70 | kill :: Ghci -> IO () 71 | kill ghci = ignored $ do 72 | timeout 5 $ do 73 | debugShutdown "Before quit" 74 | ignored $ quit ghci 75 | debugShutdown "After quit" 76 | debugShutdown "Before terminateProcess" 77 | ignored $ terminateProcess $ process ghci 78 | debugShutdown "After terminateProcess" 79 | -- Ctrl-C after a tests keeps the cursor hidden, 80 | -- `setSGR []`didn't seem to be enough 81 | -- See: https://github.com/ndmitchell/ghcid/issues/254 82 | showCursor 83 | 84 | loadedModules :: FilePath -> [Load] -> [FilePath] 85 | loadedModules dir = nubOrd . map (loadFile . qualify dir) . filter predicate 86 | where 87 | predicate Message{loadFile = loadFile} = loadFile /= "" 88 | predicate Loading{loadFile = loadFile} = loadFile /= "" 89 | predicate _ = False 90 | 91 | qualify :: FilePath -> Load -> Load 92 | qualify dir message = message{loadFile = dir loadFile message} 93 | 94 | -- | Spawn a new Ghci process at a given command line. Returns the load messages, plus 95 | -- the list of files that were observed (both those loaded and those that failed to load). 96 | sessionStart :: Session -> String -> [String] -> IO ([Load], [FilePath]) 97 | sessionStart Session{..} cmd setup = do 98 | modifyVar_ running $ const $ pure False 99 | writeIORef command $ Just (cmd, setup) 100 | 101 | -- cleanup any old instances 102 | whenJustM (readIORef ghci) $ \v -> do 103 | writeIORef ghci Nothing 104 | void $ forkIO $ kill v 105 | 106 | -- start the new 107 | outStrLn $ "Loading " ++ cmd ++ " ..." 108 | (v, messages) <- mask $ \unmask -> do 109 | (v, messages) <- unmask $ startGhci cmd Nothing $ const outStrLn 110 | writeIORef ghci $ Just v 111 | pure (v, messages) 112 | 113 | -- do whatever preparation was requested 114 | exec v $ unlines setup 115 | 116 | -- deal with current directory 117 | (dir, _) <- showPaths v 118 | writeIORef curdir dir 119 | messages <- pure $ map (qualify dir) messages 120 | 121 | let loaded = loadedModules dir messages 122 | evals <- performEvals v allowEval loaded 123 | 124 | -- install a handler 125 | forkIO $ do 126 | code <- waitForProcess $ process v 127 | whenJustM (readIORef ghci) $ \ghci -> 128 | when (ghci == v) $ do 129 | sleep 0.3 -- give anyone reading from the stream a chance to throw first 130 | throwTo withThread $ ErrorCall $ "Command \"" ++ cmd ++ "\" exited unexpectedly with " ++ show code 131 | 132 | -- handle what the process returned 133 | messages <- pure $ mapMaybe tidyMessage messages 134 | writeIORef warnings $ getWarnings messages 135 | pure (messages ++ evals, loaded) 136 | 137 | 138 | getWarnings :: [Load] -> [Load] 139 | getWarnings messages = [m | m@Message{..} <- messages, loadSeverity == Warning] 140 | 141 | 142 | -- | Call 'sessionStart' at the previous command. 143 | sessionRestart :: Session -> IO ([Load], [FilePath]) 144 | sessionRestart session@Session{..} = do 145 | Just (cmd, setup) <- readIORef command 146 | sessionStart session cmd setup 147 | 148 | 149 | performEvals :: Ghci -> Bool -> [FilePath] -> IO [Load] 150 | performEvals _ False _ = pure [] 151 | performEvals ghci True reloaded = do 152 | cmds <- mapM getCommands reloaded 153 | fmap join $ forM cmds $ \(file, cmds') -> 154 | forM cmds' $ \(num, cmd) -> do 155 | ref <- newIORef [] 156 | execStream ghci cmd $ \_ resp -> modifyIORef ref (resp :) 157 | resp <- unlines . reverse <$> readIORef ref 158 | pure $ Eval $ EvalResult file (num, 1) cmd resp 159 | 160 | 161 | getCommands :: FilePath -> IO (FilePath, [(Int, String)]) 162 | getCommands fp = do 163 | ls <- readFileUTF8' fp 164 | pure (fp, splitCommands $ zipFrom 1 $ lines ls) 165 | 166 | splitCommands :: [(Int, String)] -> [(Int, String)] 167 | splitCommands [] = [] 168 | splitCommands ((num, line) : ls) 169 | | isCommand line = 170 | let (cmds, xs) = span (isCommand . snd) ls 171 | in (num, unwords $ fmap (drop $ length commandPrefix) $ line : fmap snd cmds) : splitCommands xs 172 | | isMultilineCommandPrefix line = 173 | let (cmds, xs) = break (isMultilineCommandSuffix . snd) ls 174 | in (num, unlines (wrapGhciMultiline (fmap snd cmds))) : splitCommands (drop1 xs) 175 | | otherwise = splitCommands ls 176 | 177 | isCommand :: String -> Bool 178 | isCommand = isPrefixOf commandPrefix 179 | 180 | commandPrefix :: String 181 | commandPrefix = "-- $> " 182 | 183 | isMultilineCommandPrefix :: String -> Bool 184 | isMultilineCommandPrefix = (==) multilineCommandPrefix 185 | 186 | multilineCommandPrefix :: String 187 | multilineCommandPrefix = "{- $>" 188 | 189 | isMultilineCommandSuffix :: String -> Bool 190 | isMultilineCommandSuffix = (==) multilineCommandSuffix 191 | 192 | multilineCommandSuffix :: String 193 | multilineCommandSuffix = "<$ -}" 194 | 195 | wrapGhciMultiline :: [String] -> [String] 196 | wrapGhciMultiline xs = [":{"] ++ xs ++ [":}"] 197 | 198 | -- | Reload, returning the same information as 'sessionStart'. In particular, any 199 | -- information that GHCi doesn't repeat (warnings from loaded modules) will be 200 | -- added back in. 201 | sessionReload :: Session -> IO ([Load], [FilePath], [FilePath]) 202 | sessionReload session@Session{..} = do 203 | -- kill anything async, set stuck if you didn't succeed 204 | old <- modifyVar running $ \b -> pure (False, b) 205 | stuck <- if not old then pure False else do 206 | Just ghci <- readIORef ghci 207 | fmap isNothing $ timeout 5 $ interrupt ghci 208 | 209 | if stuck 210 | then (\(messages,loaded) -> (messages,loaded,loaded)) <$> sessionRestart session 211 | else do 212 | -- actually reload 213 | Just ghci <- readIORef ghci 214 | dir <- readIORef curdir 215 | messages <- mapMaybe tidyMessage <$> reload ghci 216 | loaded <- map ((dir ) . snd) <$> showModules ghci 217 | let reloaded = loadedModules dir messages 218 | warn <- readIORef warnings 219 | evals <- performEvals ghci allowEval reloaded 220 | 221 | -- only keep old warnings from files that are still loaded, but did not reload 222 | let validWarn w = loadFile w `elem` loaded && loadFile w `notElem` reloaded 223 | -- newest warnings always go first, so the file you hit save on most recently has warnings first 224 | messages <- pure $ messages ++ filter validWarn warn 225 | 226 | writeIORef warnings $ getWarnings messages 227 | pure (messages ++ evals, nubOrd (loaded ++ reloaded), reloaded) 228 | 229 | 230 | -- | Run an exec operation asynchronously. Should not be a @:reload@ or similar. 231 | -- Will be automatically aborted if it takes too long. Only fires done if not aborted. 232 | -- Argument to done is the final stderr line. 233 | sessionExecAsync :: Session -> String -> (String -> IO ()) -> IO () 234 | sessionExecAsync Session{..} cmd done = do 235 | Just ghci <- readIORef ghci 236 | stderr <- newIORef "" 237 | modifyVar_ running $ const $ pure True 238 | caller <- myThreadId 239 | void $ flip forkFinally (either (throwTo caller) (const $ pure ())) $ do 240 | execStream ghci cmd $ \strm msg -> 241 | when (msg /= "*** Exception: ExitSuccess") $ do 242 | when (strm == Stderr) $ writeIORef stderr msg 243 | outStrLn msg 244 | old <- modifyVar running $ \b -> pure (False, b) 245 | -- don't fire Done if someone interrupted us 246 | stderr <- readIORef stderr 247 | when old $ done stderr 248 | 249 | 250 | -- | Ignore entirely pointless messages and remove unnecessary lines. 251 | tidyMessage :: Load -> Maybe Load 252 | tidyMessage Message{loadSeverity=Warning, loadMessage=[_,x]} 253 | | unescape x == " -O conflicts with --interactive; -O ignored." = Nothing 254 | tidyMessage m@Message{..} 255 | = Just m{loadMessage = filter (\x -> not $ any (`isPrefixOf` unescape x) bad) loadMessage} 256 | where bad = [" except perhaps to import instances from" 257 | ," To import instances alone, use: import "] 258 | tidyMessage x = Just x 259 | -------------------------------------------------------------------------------- /src/Test.hs: -------------------------------------------------------------------------------- 1 | 2 | module Test(main) where 3 | 4 | import Test.Tasty 5 | import System.IO 6 | import System.Console.CmdArgs 7 | 8 | import Test.Util 9 | import Test.Parser 10 | import Test.API 11 | import Test.Ghcid 12 | 13 | main :: IO () 14 | main = do 15 | hSetBuffering stdout NoBuffering 16 | setVerbosity Loud 17 | defaultMain tests 18 | 19 | tests :: TestTree 20 | tests = testGroup "Tests" $ take 3 -- TEMPORARY 21 | [utilsTests 22 | ,parserTests 23 | ,apiTests 24 | ,ghcidTest 25 | ] 26 | -------------------------------------------------------------------------------- /src/Test/API.hs: -------------------------------------------------------------------------------- 1 | -- | Test the high level library API 2 | module Test.API(apiTests) where 3 | 4 | import Test.Tasty 5 | import Test.Tasty.HUnit 6 | import System.FilePath 7 | import System.IO.Extra 8 | import System.Time.Extra 9 | import Language.Haskell.Ghcid 10 | import Language.Haskell.Ghcid.Util 11 | import Test.Common 12 | 13 | 14 | apiTests :: TestTree 15 | apiTests = testGroup "API test" 16 | [testCase "No files" $ withTempDir $ \dir -> do 17 | (ghci,load) <- startGhci "ghci -ignore-dot-ghci" (Just dir) $ const putStrLn 18 | load @?= [] 19 | showModules ghci >>= (@?= []) 20 | exec ghci "import Data.List" 21 | exec ghci "nub \"test\"" >>= (@?= ["\"tes\""]) 22 | stopGhci ghci 23 | 24 | ,disable19650 $ testCase "Load file" $ withTempDir $ \dir -> do 25 | writeFile (dir "File.hs") "module A where\na = 123" 26 | (ghci, load) <- startGhci "ghci -ignore-dot-ghci File.hs" (Just dir) $ const putStrLn 27 | load @?= [Loading "A" "File.hs"] 28 | exec ghci "a + 1" >>= (@?= ["124"]) 29 | reload ghci >>= (@?= []) 30 | 31 | sleep =<< getModTimeResolution 32 | writeFile (dir "File.hs") "module A where\na = 456" 33 | exec ghci "a + 1" >>= (@?= ["124"]) 34 | reload ghci >>= (@?= [Loading "A" "File.hs"]) 35 | exec ghci "a + 1" >>= (@?= ["457"]) 36 | stopGhci ghci 37 | ] 38 | -------------------------------------------------------------------------------- /src/Test/Common.hs: -------------------------------------------------------------------------------- 1 | 2 | module Test.Common(disable19650) where 3 | 4 | import Test.Tasty 5 | import System.Info 6 | import Data.Version 7 | 8 | -- Tests which are disabled due to https://gitlab.haskell.org/ghc/ghc/-/issues/19650 9 | -- readily resetting which packages are loaded 10 | disable19650 :: TestTree -> TestTree 11 | disable19650 x 12 | | compilerVersion < makeVersion [9] = x 13 | | otherwise = testGroup "Disabled" [] 14 | -------------------------------------------------------------------------------- /src/Test/Ghcid.hs: -------------------------------------------------------------------------------- 1 | 2 | -- | Test behavior of the executable, polling files for changes 3 | module Test.Ghcid(ghcidTest) where 4 | 5 | import Control.Concurrent.Extra 6 | import Control.Exception.Extra 7 | import Control.Monad.Extra 8 | import Data.Char 9 | import Data.List.Extra 10 | import System.Directory.Extra 11 | import System.IO.Extra 12 | import System.Time.Extra 13 | import Data.Version.Extra 14 | import System.Environment 15 | import System.Process.Extra 16 | import System.FilePath 17 | import System.Exit 18 | 19 | import Test.Tasty 20 | import Test.Tasty.HUnit 21 | 22 | import Ghcid 23 | import Language.Haskell.Ghcid.Escape 24 | import Language.Haskell.Ghcid.Util 25 | import Test.Common 26 | import Data.Functor 27 | import Prelude 28 | 29 | 30 | ghcidTest :: TestTree 31 | ghcidTest = testGroup "Ghcid test" 32 | [basicTest 33 | ,cdTest 34 | ,dotGhciTest 35 | ,cabalTest 36 | ,stackTest 37 | ] 38 | 39 | 40 | freshDir :: IO a -> IO a 41 | freshDir act = withTempDir $ \tdir -> withCurrentDirectory tdir act 42 | 43 | copyDir :: FilePath -> IO a -> IO () 44 | copyDir dir act = do 45 | b <- doesDirectoryExist dir 46 | if not b then putStrLn $ "Couldn't run test because test source is missing, " ++ dir else void $ 47 | withTempDir $ \tdir -> do 48 | xs <- withCurrentDirectory dir $ listFilesRecursive "." 49 | forM_ xs $ \x -> do 50 | createDirectoryIfMissing True $ takeDirectory $ tdir x 51 | copyFile (dir x) (tdir x) 52 | withCurrentDirectory tdir act 53 | 54 | 55 | whenExecutable :: String -> IO a -> IO () 56 | whenExecutable exe act = do 57 | v <- findExecutable exe 58 | case v of 59 | Nothing -> putStrLn $ "Couldn't run test because " ++ exe ++ " is missing" 60 | Just _ -> void act 61 | 62 | 63 | withGhcid :: [String] -> (([String] -> IO ()) -> IO a) -> IO a 64 | withGhcid args script = do 65 | chan <- newChan 66 | let require want = do 67 | t <- timeout 30 $ readChan chan 68 | case t of 69 | Nothing -> fail $ "Require failed to produce results in time, expected: " ++ show want 70 | Just got -> assertApproxInfix want got 71 | sleep =<< getModTimeResolution 72 | 73 | let output msg = do 74 | let msg2 = filter (/= "") msg 75 | putStr $ unlines $ map ("%PRINT: "++) msg2 76 | writeChan chan msg2 77 | done <- newBarrier 78 | res <- bracket 79 | (flip forkFinally (const $ signalBarrier done ()) $ 80 | withArgs (["--no-title","--no-status"]++args) $ 81 | mainWithTerminal (pure $ TermSize 100 (Just 50) WrapHard) output) 82 | killThread $ \_ -> script require 83 | waitBarrier done 84 | pure res 85 | 86 | 87 | -- | Since different versions of GHCi give different messages, we only try to find what 88 | -- we require anywhere in the obtained messages, ignoring weird characters. 89 | assertApproxInfix :: [String] -> [String] -> IO () 90 | assertApproxInfix want got = do 91 | -- Spacing and quotes tend to be different on different GHCi versions 92 | let simple = lower . filter (\x -> isLetter x || isDigit x || x == ':') . unescape 93 | got2 = simple $ unwords got 94 | all ((`isInfixOf` got2) . simple) want @? 95 | "Expected " ++ show want ++ ", got " ++ show got 96 | 97 | 98 | write :: FilePath -> String -> IO () 99 | write file x = do 100 | print ("writeFile",file,x) 101 | createDirectoryIfMissing True $ takeDirectory file 102 | writeFile file x 103 | 104 | append :: FilePath -> String -> IO () 105 | append file x = do 106 | print ("appendFile",file,x) 107 | appendFile file x 108 | 109 | rename :: FilePath -> FilePath -> IO () 110 | rename from to = do 111 | print ("renameFile",from,to) 112 | renameFile from to 113 | 114 | 115 | 116 | --------------------------------------------------------------------- 117 | -- ACTUAL TEST SUITE 118 | 119 | basicTest :: TestTree 120 | basicTest = disable19650 $ testCase "Ghcid basic" $ freshDir $ do 121 | write "Main.hs" "main = print 1" 122 | withGhcid ["-cghci -fwarn-unused-binds Main.hs"] $ \require -> do 123 | require [allGoodMessage] 124 | write "Main.hs" "x" 125 | require ["Main.hs:1:1"," Parse error:"] 126 | 127 | -- Github issue 275 128 | write "Main.hs" "{-# LINE 42 \"foo.bar\" #-}\nx" 129 | require ["foo.bar:42:1", "Parse error:"] 130 | 131 | write "Util.hs" "module Util where" 132 | write "Main.hs" "import Util\nmain = print 1" 133 | require [allGoodMessage] 134 | write "Util.hs" "module Util where\nx" 135 | require ["Util.hs:2:1","Parse error:"] 136 | write "Util.hs" "module Util() where\nx = 1" 137 | require ["Util.hs:2:1","Warning:","Defined but not used: `x'"] 138 | 139 | -- check warnings persist properly 140 | write "Main.hs" "import Util\nx" 141 | require ["Main.hs:2:1","Parse error:" 142 | ,"Util.hs:2:1","Warning:","Defined but not used: `x'"] 143 | write "Main.hs" "import Util\nmain = print 2" 144 | require ["Util.hs:2:1","Warning:","Defined but not used: `x'"] 145 | write "Main.hs" "main = print 3" 146 | require [allGoodMessage] 147 | write "Main.hs" "import Util\nmain = print 4" 148 | require ["Util.hs:2:1","Warning:","Defined but not used: `x'"] 149 | write "Util.hs" "module Util where" 150 | require [allGoodMessage] 151 | 152 | -- check recursive modules work 153 | write "Util.hs" "module Util where\nimport Main" 154 | require ["imports form a cycle","Main.hs","Util.hs"] 155 | write "Util.hs" "module Util where" 156 | require [allGoodMessage] 157 | 158 | ghcVer <- readVersion <$> systemOutput_ "ghc --numeric-version" 159 | 160 | -- check renaming files works 161 | when (ghcVer < makeVersion [8]) $ do 162 | -- note that due to GHC bug #9648 and #11596 this doesn't work with newer GHC 163 | -- see https://ghc.haskell.org/trac/ghc/ticket/11596 164 | rename "Util.hs" "Util2.hs" 165 | require ["Main.hs:1:8","Could not find module `Util'"] 166 | rename "Util2.hs" "Util.hs" 167 | require [allGoodMessage] 168 | 169 | -- after this point GHC bugs mean nothing really works too much 170 | 171 | 172 | cdTest :: TestTree 173 | cdTest = disable19650 $ testCase "Cd basic" $ freshDir $ do 174 | write "foo/Main.hs" "main = print 1" 175 | write "foo/Util.hs" "import Bob" 176 | write "foo/.ghci" ":load Main" 177 | ignore $ void $ system "chmod go-w foo foo/.ghci" 178 | ghcVer <- readVersion <$> systemOutput_ "ghc --numeric-version" 179 | -- GHC 8.0 and lower don't emit the LoadConfig messages 180 | withGhcid ("-ccd foo && ghci" : ["--restart=foo/.ghci" | ghcVer < makeVersion [8,2]]) $ \require -> do 181 | require [allGoodMessage] 182 | write "foo/Main.hs" "x" 183 | require ["Main.hs:1:1"," Parse error:"] 184 | write "foo/.ghci" ":load Util" 185 | require ["Util.hs:1:","`Bob'"] 186 | 187 | 188 | dotGhciTest :: TestTree 189 | dotGhciTest = testCase "Ghcid .ghci" $ copyDir "test/foo" $ do 190 | write "test.txt" "" 191 | ignore $ void $ system "chmod go-w .ghci" 192 | withGhcid ["--test=:test"] $ \require -> do 193 | require [allGoodMessage] 194 | sleep 1 -- time to write out the test 195 | readFile "test.txt" >>= (@?= "X") -- the test writes out X 196 | append "Test.hs" "\n" 197 | require [allGoodMessage] 198 | sleep 1 -- time to write out the test 199 | readFile "test.txt" >>= (@?= "XX") 200 | print =<< readFile ".ghci" 201 | write ".ghci" ":set -fwarn-unused-imports\n:load Root Paths.hs Test" 202 | require ["The import of Paths_foo is redundant"] 203 | sleep 1 -- time to write out the test 204 | readFile "test.txt" >>= (@?= "XX") -- but shouldn't run on warning 205 | 206 | 207 | cabalTest :: TestTree 208 | cabalTest = testCase "Ghcid Cabal" $ copyDir "test/bar" $ whenExecutable "cabal" $ do 209 | env <- getEnvironment 210 | let db = ["--package-db=" ++ x | x <- maybe [] splitSearchPath $ lookup "GHC_PACKAGE_PATH" env] 211 | (_, _, _, pid) <- createProcess $ 212 | (proc "cabal" $ "configure":db){env = Just $ filter ((/=) "GHC_PACKAGE_PATH" . fst) env} 213 | ExitSuccess <- waitForProcess pid 214 | 215 | withGhcid [] $ \require -> do 216 | require [allGoodMessage] 217 | orig <- readFile' "src/Literate.lhs" 218 | append "src/Literate.lhs" "> x" 219 | require ["src/Literate.lhs:5:3","Parse error:"] 220 | write "src/Literate.lhs" orig 221 | require [allGoodMessage] 222 | 223 | stackTest :: TestTree 224 | stackTest = testCase "Ghcid Stack" $ copyDir "test/bar" $ whenExecutable "stack" $ do 225 | system_ "stack init --resolver=nightly" -- must match what the CI does, or it takes too long 226 | createDirectoryIfMissing True ".stack-work" 227 | 228 | withGhcid [] $ \require -> do 229 | require [allGoodMessage ++ " (4 modules, at "] 230 | -- the .ghci file we watch was created _after_ we started loading stack 231 | -- so ghcid is correct to immediately reload, in case it changed 232 | require [allGoodMessage ++ " (4 modules, at "] 233 | append "src/Literate.lhs" "> x" 234 | require ["src/Literate.lhs:5:3","Parse error:"] 235 | {- 236 | -- Stack seems to have changed, and continues to do so - lets just test the basics 237 | withGhcid ["src/Boot.hs"] $ \require -> do 238 | require [allGoodMessage] 239 | writeFile "src/Boot.hs" "X" 240 | require ["src/Boot.hs:1:1","Parse error:"] 241 | -} 242 | -------------------------------------------------------------------------------- /src/Test/Parser.hs: -------------------------------------------------------------------------------- 1 | -- | Test the message parser 2 | module Test.Parser(parserTests) where 3 | 4 | import Test.Tasty 5 | import Test.Tasty.HUnit 6 | 7 | import Language.Haskell.Ghcid.Parser 8 | import Language.Haskell.Ghcid.Types 9 | 10 | 11 | parserTests :: TestTree 12 | parserTests = testGroup "Parser tests" 13 | [testParseShowModules 14 | ,testParseShowPaths 15 | ,testParseLoad 16 | ,testParseLoadGhc82 17 | ,testParseLoadSpans 18 | ,testParseLoadCycles 19 | ,testParseLoadCyclesSelf 20 | ,testParseLoadEscapeCodes 21 | ,testMissingFile 22 | ] 23 | 24 | testParseShowModules :: TestTree 25 | testParseShowModules = testCase "Show Modules" $ parseShowModules 26 | ["Main ( src/Main.hs, interpreted )" 27 | ,"AI.Neural.WiscDigit ( src/AI/Neural/WiscDigit.hs, interpreted )" 28 | ] @?= 29 | [("Main","src/Main.hs") 30 | ,("AI.Neural.WiscDigit","src/AI/Neural/WiscDigit.hs") 31 | ] 32 | 33 | testParseShowPaths :: TestTree 34 | testParseShowPaths = testCase "Show Paths" $ parseShowPaths 35 | ["current working directory:" 36 | ," C:\\Neil\\ghcid" 37 | ,"module import search paths:" 38 | ," ." 39 | ," src" 40 | ] @?= 41 | ("C:\\Neil\\ghcid",[".","src"]) 42 | 43 | testParseLoad :: TestTree 44 | testParseLoad = testCase "Load Parsing" $ parseLoad 45 | ["[1 of 2] Compiling GHCi ( GHCi.hs, interpreted )" 46 | ,"GHCi.hs:70:1: Parse error: naked expression at top level" 47 | ,"GHCi.hs:72:13:" 48 | ," No instance for (Num ([String] -> [String]))" 49 | ," arising from the literal `1'" 50 | ," Possible fix:" 51 | ," add an instance declaration for (Num ([String] -> [String]))" 52 | ," In the expression: 1" 53 | ," In an equation for `parseLoad': parseLoad = 1" 54 | ,"GHCi.hs:81:1: Warning: Defined but not used: `foo'" 55 | ,"C:\\GHCi.hs:82:1: warning: Defined but not used: \8216foo\8217" -- GHC 7.12 uses lowercase 56 | ,"src\\Haskell.hs:4:23:" 57 | ," Warning: {-# SOURCE #-} unnecessary in import of `Boot'" 58 | ,"src\\Boot.hs-boot:2:8:" 59 | ," File name does not match module name:" 60 | ," Saw: `BootX'" 61 | ," Expected: `Boot'" 62 | ] @?= 63 | [Loading "GHCi" "GHCi.hs" 64 | ,Message Error "GHCi.hs" (70,1) (70,1) 65 | ["GHCi.hs:70:1: Parse error: naked expression at top level"] 66 | ,Message Error "GHCi.hs" (72,13) (72,13) 67 | ["GHCi.hs:72:13:" 68 | ," No instance for (Num ([String] -> [String]))" 69 | ," arising from the literal `1'"," Possible fix:" 70 | ," add an instance declaration for (Num ([String] -> [String]))" 71 | ," In the expression: 1" 72 | ," In an equation for `parseLoad': parseLoad = 1"] 73 | ,Message Warning "GHCi.hs" (81,1) (81,1) 74 | ["GHCi.hs:81:1: Warning: Defined but not used: `foo'"] 75 | ,Message Warning "C:\\GHCi.hs" (82,1) (82,1) 76 | ["C:\\GHCi.hs:82:1: warning: Defined but not used: \8216foo\8217"] 77 | ,Message Warning "src\\Haskell.hs" (4,23) (4,23) 78 | ["src\\Haskell.hs:4:23:" 79 | ," Warning: {-# SOURCE #-} unnecessary in import of `Boot'"] 80 | ,Message Error "src\\Boot.hs-boot" (2,8) (2,8) 81 | ["src\\Boot.hs-boot:2:8:" 82 | ," File name does not match module name:" 83 | ," Saw: `BootX'" 84 | ," Expected: `Boot'"] 85 | ] 86 | 87 | testParseLoadGhc82 :: TestTree 88 | testParseLoadGhc82 = testCase "GHC 8.2 Load Parsing" $ parseLoad 89 | ["[18 of 24] Compiling Physics ( Physics.hs, interpreted )" 90 | ,"Physics.hs:30:18: error: parse error on input ‘^*’" 91 | ," |" 92 | ,"30 | dx = ' ^* delta" 93 | ," | ^^" 94 | ,"Loaded GHCi configuration from C:\\Neil\\ghcid\\.ghci" 95 | ] @?= 96 | [Loading "Physics" "Physics.hs" 97 | ,Message Error "Physics.hs" (30,18) (30,18) 98 | ["Physics.hs:30:18: error: parse error on input ‘^*’" 99 | ," |" 100 | ,"30 | dx = ' ^* delta" 101 | ," | ^^"] 102 | ,LoadConfig "C:\\Neil\\ghcid\\.ghci" 103 | ] 104 | 105 | testMissingFile = testCase "Starting ghci with a non-existent filename" $ parseLoad 106 | [": error: can't find file: bob.hs" 107 | ] @?= 108 | [Message Error "" (0,0) (0,0) [": error: can't find file: bob.hs"]] 109 | 110 | testParseLoadCyclesSelf = testCase "Module cycle with itself" $ parseLoad 111 | ["Module imports form a cycle:" 112 | ," module `Language.Haskell.Ghcid.Parser' (src\\Language\\Haskell\\Ghcid\\Parser.hs) imports itself" 113 | ] @?= 114 | [Message Error "src\\Language\\Haskell\\Ghcid\\Parser.hs" (0,0) (0,0) 115 | ["Module imports form a cycle:" 116 | ," module `Language.Haskell.Ghcid.Parser' (src\\Language\\Haskell\\Ghcid\\Parser.hs) imports itself"] 117 | ] 118 | 119 | testParseLoadCycles = testCase "Module cycle" $ parseLoad 120 | ["[ 4 of 13] Compiling Language.Haskell.Ghcid.Parser ( src\\Language\\Haskell\\Ghcid\\Parser.hs, interpreted )" 121 | ,"Module imports form a cycle:" 122 | ," module `Language.Haskell.Ghcid.Util' (src\\Language\\Haskell\\Ghcid\\Util.hs)" 123 | ," imports `Language.Haskell.Ghcid' (src\\Language\\Haskell\\Ghcid.hs)" 124 | ," which imports `Language.Haskell.Ghcid.Util' (src\\Language\\Haskell\\Ghcid\\Util.hs)" 125 | ] @?= 126 | let msg = ["Module imports form a cycle:" 127 | ," module `Language.Haskell.Ghcid.Util' (src\\Language\\Haskell\\Ghcid\\Util.hs)" 128 | ," imports `Language.Haskell.Ghcid' (src\\Language\\Haskell\\Ghcid.hs)" 129 | ," which imports `Language.Haskell.Ghcid.Util' (src\\Language\\Haskell\\Ghcid\\Util.hs)"] in 130 | [Loading "Language.Haskell.Ghcid.Parser" "src\\Language\\Haskell\\Ghcid\\Parser.hs" 131 | ,Message Error "src\\Language\\Haskell\\Ghcid\\Util.hs" (0,0) (0,0) msg 132 | ,Message Error "src\\Language\\Haskell\\Ghcid.hs" (0,0) (0,0) msg 133 | ] 134 | 135 | testParseLoadEscapeCodes = testCase "Escape codes as enabled by -fdiagnostics-color=always" $ parseLoad 136 | ["\ESC[;1msrc\\Language\\Haskell\\Ghcid\\Types.hs:11:1: \ESC[;1m\ESC[35mwarning:\ESC[0m\ESC[0m\ESC[;1m [\ESC[;1m\ESC[35m-Wunused-imports\ESC[0m\ESC[0m\ESC[;1m]\ESC[0m\ESC[0m\ESC[;1m" 137 | ," The import of `Data.Data' is redundant" 138 | ," except perhaps to import instances from `Data.Data'" 139 | ," To import instances alone, use: import Data.Data()\ESC[0m\ESC[0m" 140 | ,"\ESC[;1m\ESC[34m |\ESC[0m\ESC[0m" 141 | ,"\ESC[;1m\ESC[34m11 |\ESC[0m\ESC[0m \ESC[;1m\ESC[35mimport Data.Data\ESC[0m\ESC[0m" 142 | ,"\ESC[;1m\ESC[34m |\ESC[0m\ESC[0m\ESC[;1m\ESC[35m ^^^^^^^^^^^^^^^^\ESC[0m\ESC[0m" 143 | ,"\ESC[0m\ESC[0m\ESC[0m" 144 | ,"\ESC[;1msrc\\Language\\Haskell\\Ghcid\\Util.hs:11:1: \ESC[;1m\ESC[31merror:\ESC[0m\ESC[0m\ESC[;1m\ESC[0m\ESC[0m\ESC[;1m" 145 | ," Could not find module `Language.Haskell.Ghcid.None'\ESC[0m\ESC[0m" 146 | ,"\ESC[;1m\ESC[34m |\ESC[0m\ESC[0m" 147 | ,"\ESC[;1m\ESC[34m11 |\ESC[0m\ESC[0m \ESC[;1m\ESC[31mimport Language.Haskell.Ghcid.None\ESC[0m\ESC[0m" 148 | ,"\ESC[;1m\ESC[34m |\ESC[0m\ESC[0m\ESC[;1m\ESC[31m ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\ESC[0m\ESC[0m" 149 | ,"\ESC[0m\ESC[0m\ESC[0m" 150 | ] @?= 151 | [Message Warning "src\\Language\\Haskell\\Ghcid\\Types.hs" (11,1) (11,1) 152 | ["\ESC[;1msrc\\Language\\Haskell\\Ghcid\\Types.hs:11:1: \ESC[;1m\ESC[35mwarning:\ESC[0m\ESC[0m\ESC[;1m [\ESC[;1m\ESC[35m-Wunused-imports\ESC[0m\ESC[0m\ESC[;1m]\ESC[0m\ESC[0m\ESC[;1m" 153 | ," The import of `Data.Data' is redundant" 154 | ," except perhaps to import instances from `Data.Data'" 155 | ," To import instances alone, use: import Data.Data()\ESC[0m\ESC[0m" 156 | ,"\ESC[;1m\ESC[34m |\ESC[0m\ESC[0m","\ESC[;1m\ESC[34m11 |\ESC[0m\ESC[0m \ESC[;1m\ESC[35mimport Data.Data\ESC[0m\ESC[0m" 157 | ,"\ESC[;1m\ESC[34m |\ESC[0m\ESC[0m\ESC[;1m\ESC[35m ^^^^^^^^^^^^^^^^\ESC[0m\ESC[0m"] 158 | ,Message Error "src\\Language\\Haskell\\Ghcid\\Util.hs" (11,1) (11,1) 159 | ["\ESC[;1msrc\\Language\\Haskell\\Ghcid\\Util.hs:11:1: \ESC[;1m\ESC[31merror:\ESC[0m\ESC[0m\ESC[;1m\ESC[0m\ESC[0m\ESC[;1m" 160 | ," Could not find module `Language.Haskell.Ghcid.None'\ESC[0m\ESC[0m" 161 | ,"\ESC[;1m\ESC[34m |\ESC[0m\ESC[0m","\ESC[;1m\ESC[34m11 |\ESC[0m\ESC[0m \ESC[;1m\ESC[31mimport Language.Haskell.Ghcid.None\ESC[0m\ESC[0m" 162 | ,"\ESC[;1m\ESC[34m |\ESC[0m\ESC[0m\ESC[;1m\ESC[31m ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\ESC[0m\ESC[0m"] 163 | ] 164 | 165 | testParseLoadSpans :: TestTree 166 | testParseLoadSpans = testCase "Load Parsing when -ferror-spans is enabled" $ parseLoad 167 | ["[1 of 2] Compiling GHCi ( GHCi.hs, interpreted )" 168 | ,"GHCi.hs:70:1-2: Parse error: naked expression at top level" 169 | ,"GHCi.hs:72:13-14:" 170 | ," No instance for (Num ([String] -> [String]))" 171 | ," arising from the literal `1'" 172 | ," Possible fix:" 173 | ," add an instance declaration for (Num ([String] -> [String]))" 174 | ," In the expression: 1" 175 | ," In an equation for `parseLoad': parseLoad = 1" 176 | ,"GHCi.hs:81:1-15: Warning: Defined but not used: `foo'" 177 | ,"C:\\GHCi.hs:82:1-17: warning: Defined but not used: \8216foo\8217" -- GHC 7.12 uses lowercase 178 | ,"src\\Haskell.hs:4:23-24:" 179 | ," Warning: {-# SOURCE #-} unnecessary in import of `Boot'" 180 | ,"src\\Boot.hs-boot:2:8-5:" 181 | ," File name does not match module name:" 182 | ," Saw: `BootX'" 183 | ," Expected: `Boot'" 184 | ,"/src/TrieSpec.hs:(192,7)-(193,76): Warning:" 185 | ," A do-notation statement discarded a result of type ‘[()]’" 186 | ] @?= 187 | [Loading "GHCi" "GHCi.hs" 188 | ,Message Error "GHCi.hs" (70,1) (70,2) 189 | ["GHCi.hs:70:1-2: Parse error: naked expression at top level"] 190 | ,Message Error "GHCi.hs" (72,13) (72,14) 191 | ["GHCi.hs:72:13-14:" 192 | ," No instance for (Num ([String] -> [String]))" 193 | ," arising from the literal `1'" 194 | ," Possible fix:" 195 | ," add an instance declaration for (Num ([String] -> [String]))" 196 | ," In the expression: 1" 197 | ," In an equation for `parseLoad': parseLoad = 1"] 198 | ,Message Warning "GHCi.hs" (81,1) (81,15) 199 | ["GHCi.hs:81:1-15: Warning: Defined but not used: `foo'"] 200 | ,Message Warning "C:\\GHCi.hs" (82,1) (82,17) 201 | ["C:\\GHCi.hs:82:1-17: warning: Defined but not used: \8216foo\8217"] 202 | ,Message Warning "src\\Haskell.hs" (4,23) (4,24) 203 | ["src\\Haskell.hs:4:23-24:" 204 | ," Warning: {-# SOURCE #-} unnecessary in import of `Boot'"] 205 | ,Message Error "src\\Boot.hs-boot" (2,8) (2,5) 206 | ["src\\Boot.hs-boot:2:8-5:" 207 | ," File name does not match module name:" 208 | ," Saw: `BootX'" 209 | ," Expected: `Boot'"] 210 | ,Message Warning "/src/TrieSpec.hs" (192,7) (193,76) 211 | ["/src/TrieSpec.hs:(192,7)-(193,76): Warning:" 212 | ," A do-notation statement discarded a result of type ‘[()]’"] 213 | ] 214 | -------------------------------------------------------------------------------- /src/Test/Util.hs: -------------------------------------------------------------------------------- 1 | 2 | -- | Test utility functions 3 | module Test.Util(utilsTests) where 4 | 5 | import Test.Tasty 6 | import Test.Tasty.HUnit 7 | 8 | import Language.Haskell.Ghcid.Util 9 | import Language.Haskell.Ghcid.Escape 10 | 11 | utilsTests :: TestTree 12 | utilsTests = testGroup "Utility tests" 13 | [dropPrefixTests 14 | ,wordWrapTests 15 | ] 16 | 17 | dropPrefixTests :: TestTree 18 | dropPrefixTests = testGroup "dropPrefix" 19 | [testCase "Prefix not found" $ dropPrefixRepeatedly "prefix" "string" @?= "string" 20 | ,testCase "Empty prefix" $ dropPrefixRepeatedly "" "string" @?= "string" 21 | ,testCase "Prefix found once" $ dropPrefixRepeatedly "str" "string" @?= "ing" 22 | ,testCase "Prefix found twice" $ dropPrefixRepeatedly "str" "strstring" @?= "ing" 23 | ] 24 | 25 | wordWrapTests :: TestTree 26 | wordWrapTests = testGroup "wordWrap" 27 | [testCase "Max 0" $ wordWrapE 4 0 (Esc "ab cd efgh") @?= [s"ab c",s"d ef",h"gh"] 28 | ,testCase "Max 2" $ wordWrapE 4 2 (Esc "ab cd efgh") @?= [h"ab ",h"cd ",h"efgh"] 29 | ] 30 | where h x = (Esc x, WrapHard) 31 | s x = (Esc x, WrapSoft) 32 | -------------------------------------------------------------------------------- /src/Wait.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ScopedTypeVariables #-} 2 | {-# LANGUAGE TupleSections #-} 3 | 4 | -- | Use 'withWaiterPoll' or 'withWaiterNotify' to create a 'Waiter' object, 5 | -- then access it (single-threaded) by using 'waitFiles'. 6 | module Wait(Waiter, withWaiterPoll, withWaiterNotify, waitFiles) where 7 | 8 | import Control.Concurrent.Extra 9 | import qualified Data.Map as Map 10 | import qualified Data.Set as Set 11 | import Control.Monad.Extra 12 | import Data.List.Extra 13 | import System.FilePath 14 | import Control.Exception.Extra 15 | import System.Directory.Extra 16 | import Data.Time.Clock 17 | import Data.String 18 | import System.Console.CmdArgs 19 | import System.Time.Extra 20 | import System.FSNotify 21 | import Language.Haskell.Ghcid.Util 22 | 23 | 24 | data Waiter 25 | = WaiterPoll Seconds 26 | | WaiterNotify WatchManager (MVar ()) (Var (Map.Map FilePath StopListening)) 27 | 28 | withWaiterPoll :: Seconds -> (Waiter -> IO a) -> IO a 29 | withWaiterPoll x f = f $ WaiterPoll x 30 | 31 | withWaiterNotify :: (Waiter -> IO a) -> IO a 32 | withWaiterNotify f = withManagerConf defaultConfig $ \manager -> do 33 | mvar <- newEmptyMVar 34 | var <- newVar Map.empty 35 | f $ WaiterNotify manager mvar var 36 | 37 | -- `listContentsInside test dir` will list files and directories inside `dir`, 38 | -- recursing into those subdirectories which pass `test`. 39 | -- Note that `dir` and files it directly contains are always listed, regardless of `test`. 40 | -- Subdirectories will have a trailing path separator, and are only listed if we recurse into them. 41 | listContentsInside :: (FilePath -> IO Bool) -> FilePath -> IO [FilePath] 42 | listContentsInside test dir = do 43 | (dirs,files) <- partitionM doesDirectoryExist =<< listContents dir 44 | recurse <- filterM test dirs 45 | rest <- concatMapM (listContentsInside test) recurse 46 | pure $ addTrailingPathSeparator dir : files ++ rest 47 | 48 | -- | Given the pattern: 49 | -- 50 | -- > wait <- waitFiles waiter 51 | -- > ... 52 | -- > wait ["File1.hs","File2.hs"] 53 | -- 54 | -- This continues as soon as either @File1.hs@ or @File2.hs@ changes, 55 | -- starting from when 'waitFiles' was initially called. 56 | -- 57 | -- returns a message about why you are continuing (usually a file name). 58 | waitFiles :: forall a. Ord a => Waiter -> IO ([(FilePath, a)] -> IO (Either String [(FilePath, a)])) 59 | waitFiles waiter = do 60 | base <- getCurrentTime 61 | pure $ \files -> handle onError (go base files) 62 | where 63 | onError :: IOError -> IO (Either String [(FilePath, a)]) 64 | onError e = sleep 1.0 >> pure (Left (show e)) 65 | 66 | go :: UTCTime -> [(FilePath, a)] -> IO (Either String [(FilePath, a)]) 67 | go base files = do 68 | whenLoud $ outStrLn $ "%WAITING: " ++ unwords (map fst files) 69 | -- As listContentsInside returns directories, we are waiting on them explicitly and so 70 | -- will pick up new files, as creating a new file changes the containing directory's modtime. 71 | files <- concatForM files $ \(file, a) -> 72 | ifM (doesDirectoryExist file) (fmap (,a) <$> listContentsInside (pure . not . isPrefixOf "." . takeFileName) file) (pure [(file, a)]) 73 | case waiter of 74 | WaiterPoll t -> pure () 75 | WaiterNotify manager kick mp -> do 76 | dirs <- fmap Set.fromList $ mapM canonicalizePathSafe $ nubOrd $ map (takeDirectory . fst) files 77 | modifyVar_ mp $ \mp -> do 78 | let (keep,del) = Map.partitionWithKey (\k v -> k `Set.member` dirs) mp 79 | sequence_ $ Map.elems del 80 | new <- forM (Set.toList $ dirs `Set.difference` Map.keysSet keep) $ \dir -> do 81 | can <- watchDir manager (fromString dir) (const True) $ \event -> do 82 | whenLoud $ outStrLn $ "%NOTIFY: " ++ show event 83 | void $ tryPutMVar kick () 84 | pure (dir, can) 85 | let mp2 = keep `Map.union` Map.fromList new 86 | whenLoud $ outStrLn $ "%WAITING: " ++ unwords (Map.keys mp2) 87 | pure mp2 88 | void $ tryTakeMVar kick 89 | new <- mapM (getModTime . fst) files 90 | case [x | (x,Just t) <- zip files new, t > base] of 91 | [] -> Right <$> recheck files new 92 | xs -> pure (Right xs) 93 | 94 | recheck :: [(FilePath, a)] -> [Maybe UTCTime] -> IO [(String, a)] 95 | recheck files old = do 96 | sleep 0.1 97 | case waiter of 98 | WaiterPoll t -> sleep $ max 0 $ t - 0.1 -- subtract the initial 0.1 sleep from above 99 | WaiterNotify _ kick _ -> do 100 | takeMVar kick 101 | whenLoud $ outStrLn "%WAITING: Notify signaled" 102 | new <- mapM (getModTime . fst) files 103 | case [x | (x,t1,t2) <- zip3 files old new, t1 /= t2] of 104 | [] -> recheck files new 105 | xs -> do 106 | let disappeared = [x | (x, Just _, Nothing) <- zip3 files old new] 107 | unless (null disappeared) $ do 108 | -- if someone is deleting a needed file, give them some space to put the file back 109 | -- typically caused by VIM 110 | -- but try not to 111 | whenLoud $ outStrLn $ "%WAITING: Waiting max of 1s due to file removal, " ++ unwords (nubOrd (map fst disappeared)) 112 | -- at most 20 iterations, but stop as soon as the file returns 113 | void $ flip firstJustM (replicate 20 ()) $ \_ -> do 114 | sleep 0.05 115 | new <- mapM (getModTime . fst) files 116 | pure $ if null [x | (x, Just _, Nothing) <- zip3 files old new] then Just () else Nothing 117 | pure xs 118 | 119 | 120 | canonicalizePathSafe :: FilePath -> IO FilePath 121 | canonicalizePathSafe x = canonicalizePath x `catch` \(_ :: IOError) -> pure x 122 | -------------------------------------------------------------------------------- /test/bar/bar.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: >= 1.2 2 | build-type: Simple 3 | name: bar 4 | version: 0 5 | 6 | library 7 | hs-source-dirs: src 8 | build-depends: base 9 | 10 | exposed-modules: 11 | Boot 12 | Haskell 13 | Literate 14 | -------------------------------------------------------------------------------- /test/bar/src/Boot.hs: -------------------------------------------------------------------------------- 1 | 2 | module Boot(extension) where 3 | 4 | import Haskell() 5 | 6 | extension = ".hs-boot" 7 | -------------------------------------------------------------------------------- /test/bar/src/Boot.hs-boot: -------------------------------------------------------------------------------- 1 | 2 | module Boot(extension) where 3 | 4 | extension :: String 5 | -------------------------------------------------------------------------------- /test/bar/src/Haskell.hs: -------------------------------------------------------------------------------- 1 | 2 | module Haskell(extension) where 3 | 4 | import {-# SOURCE #-} Boot() 5 | 6 | extension = ".hs" 7 | -------------------------------------------------------------------------------- /test/bar/src/Literate.lhs: -------------------------------------------------------------------------------- 1 | 2 | > module Literate(extension) where 3 | > 4 | > extension = ".lhs" 5 | -------------------------------------------------------------------------------- /test/foo/.ghci: -------------------------------------------------------------------------------- 1 | :set -fno-warn-unused-imports 2 | :load Root Paths.hs Test 3 | :def test \_ -> return "Test.main" 4 | -------------------------------------------------------------------------------- /test/foo/Paths.hs: -------------------------------------------------------------------------------- 1 | 2 | module Paths_foo() where 3 | -------------------------------------------------------------------------------- /test/foo/Root.hs: -------------------------------------------------------------------------------- 1 | 2 | module Root(main, root) where 3 | 4 | import Paths_foo 5 | 6 | main = print "Root.main" 7 | root = print "Root.root" 8 | -------------------------------------------------------------------------------- /test/foo/Test.hs: -------------------------------------------------------------------------------- 1 | 2 | module Test(main) where 3 | 4 | main = appendFile "test.txt" "X" 5 | -------------------------------------------------------------------------------- /test/project-stack/project-x.cabal: -------------------------------------------------------------------------------- 1 | build-type: Simple 2 | name: project-x 3 | version: 0 4 | 5 | library 6 | hs-source-dirs: src 7 | build-depends: base 8 | 9 | exposed-modules: 10 | ProjectX 11 | -------------------------------------------------------------------------------- /test/project-stack/src/ProjectX.hs: -------------------------------------------------------------------------------- 1 | 2 | module ProjectX(projectX) where 3 | 4 | projectX = "test" 5 | --------------------------------------------------------------------------------