├── .babelrc ├── .browserslistrc ├── .gitignore ├── .gitmodules ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── coffeelint.json ├── dist ├── main.js └── main.js.map ├── keymaps └── ide-haskell.cson ├── package.json ├── resources ├── haskell.svg └── icomoon.woff ├── spec ├── package.spec.ts ├── tsconfig.json └── tslint.json ├── src ├── actions-registry │ ├── index.ts │ └── select-action.ts ├── atom-config.d.ts ├── atom.d.ts ├── backend-status │ ├── index.tsx │ └── views │ │ ├── progress-bar.tsx │ │ └── status-icon.tsx ├── check-results-provider │ ├── editor-control.ts │ └── index.ts ├── config-params │ ├── index.ts │ ├── param-control.tsx │ ├── param-select-view.ts │ └── param-store.ts ├── editor-control │ ├── editor-overlay-manager.ts │ ├── event-table.ts │ └── index.ts ├── editor-mark-control │ └── index.ts ├── ide-haskell.ts ├── linter-support │ └── index.ts ├── output-panel │ ├── index.tsx │ └── views │ │ ├── output-panel-button.tsx │ │ ├── output-panel-buttons.tsx │ │ ├── output-panel-checkbox.tsx │ │ ├── output-panel-item.tsx │ │ └── output-panel-items.tsx ├── plugin-manager.ts ├── prettify │ ├── editor-controller.ts │ ├── index.ts │ ├── main.ts │ ├── util-cabal-format.ts │ ├── util-run-filter.ts │ └── util-stylish-haskell.ts ├── priority-registry │ └── index.ts ├── results-db │ ├── index.ts │ ├── provider.ts │ └── result-item.ts ├── status-bar │ └── index.tsx ├── tooltip-manager │ ├── index.ts │ ├── render-actions.tsx │ └── tooltip-view.tsx ├── tooltip-registry │ └── index.ts ├── upi-3 │ ├── consume.ts │ ├── index.ts │ └── instance.ts └── utils │ ├── cast.ts │ ├── element-listener.ts │ ├── index.ts │ └── message-object.ts ├── styles ├── icon.less ├── ide-haskell-decorations.less └── ide-haskell.less ├── tsconfig.json └── tslint.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/preset-env", { "useBuiltIns": false }]] 3 | } 4 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | electron 4.2.7 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | docs 5 | package-lock.json 6 | lib 7 | .parcel-cache 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "typings"] 2 | path = typings 3 | url = git@github.com:atom-haskell/typings.git 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true 3 | trailingComma: all 4 | arrowParens: always 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | ### Project specific config ### 2 | language: generic 3 | 4 | env: 5 | global: 6 | - APM_TEST_PACKAGES="" 7 | - ATOM_LINT_WITH_BUNDLED_NODE="true" 8 | 9 | matrix: 10 | - ATOM_CHANNEL=stable 11 | - ATOM_CHANNEL=beta 12 | 13 | os: 14 | - linux 15 | 16 | # Use sed to replace the SSH URL with the public URL, then initialize submodules 17 | before_install: 18 | - sed -i 's/git@github.com:/https:\/\/github.com\//' .gitmodules 19 | - git submodule update --init --recursive 20 | 21 | ### Generic setup follows ### 22 | script: 23 | - curl -s -O https://raw.githubusercontent.com/atom/ci/master/build-package.sh 24 | - chmod u+x build-package.sh 25 | - ./build-package.sh 26 | - npm run test 27 | 28 | notifications: 29 | email: 30 | on_success: never 31 | on_failure: change 32 | 33 | branches: 34 | only: 35 | - master 36 | 37 | git: 38 | depth: 10 39 | submodules: false 40 | 41 | sudo: false 42 | 43 | dist: trusty 44 | 45 | addons: 46 | apt: 47 | packages: 48 | - build-essential 49 | - fakeroot 50 | - git 51 | - libsecret-1-dev 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.7.0 2 | 3 | - Update dependencies 4 | - Clean-up internal API 5 | - s\/Atom-Typescript\/IDE-Haskell\/g 6 | - Open tooltip links via ide-haskell-hoogle 7 | 8 | ## 2.6.0 9 | 10 | - Pointer-events on tooltips 11 | - Tweak styles 12 | - More sensible registry defaults 13 | - Untie actions from tooltips 14 | - Initial support for standalone actions 15 | - Fix select list focus issues 16 | - Factor out action rendering 17 | - Better actions support 18 | - Crude actions support 19 | 20 | ## 2.5.0 21 | 22 | - Attach source data to tooltip 23 | - Bundle distribution (faster load times, smaller footprint) 24 | - Move backend status to separate module 25 | - Optimize default tabs creation 26 | - Make output panel tabs disposable 27 | - Handle promises appropriately 28 | - \[UPI\] Add declarative-style commands 29 | - Minor fixes 30 | 31 | ## 2.4.1 32 | 33 | - Limit what results are cleared on message provider reported severities 34 | - Do not recreate already existing result markers 35 | 36 | ## 2.4.0 37 | 38 | - Allow UPI declarative events to return check results 39 | - Use atom-ts-spec-runner 40 | 41 | ## 2.3.1 42 | 43 | - Raise progress priority to always show it 44 | 45 | Before, progress indicator wouldn't be shown when any component 46 | reported errors or warnings. Progress indicator priority has been 47 | raised to always show it when an operation is in progress. 48 | 49 | - Show progress status for declarative events 50 | 51 | Some events, like mouseover type tooltip, didn't always show 52 | progress indicator correctly. 53 | 54 | ## 2.3.0 55 | 56 | - Bump dependencies. Minimal Atom version is 1.24 57 | - Use HTML progress element for progress bar 58 | 59 | This makes it a little bit more consistent with the style guide. 60 | 61 | - Add configuration option for buttons position 62 | 63 | Whereas before buttons (Error/Warning/Lint/etc) position was defined 64 | by whether the panel is positioned in bottom dock or left/right one, 65 | now the behavior is controlled by a configuration option 66 | `ide-haskell.buttonsPosition`, 'Panel buttons position', which can 67 | be either 'top' (the default) or 'left'. 68 | 69 | ## 2.2.3 70 | 71 | - Fix for 72 | 73 | - Group fast-firing messages for purposes of `switchTabOnCheck` 74 | - Do not rely on severities ordering when deciding which tab to 75 | switch to if `switchTabOnCheck` is enabled 76 | - Code cleanup (strict boolean expressions) 77 | - Pin node types version 78 | - Move config schema to package.json 79 | - Add Travis CI builds 80 | - Add the most basic tests 81 | - Bump Atom version 82 | 83 | ## 2.2.2 84 | 85 | - Update atom typings; bump devDependencies 86 | - Add lodash to dependencies 87 | 88 | ## 2.2.1 89 | 90 | - Show currently-selected param value 91 | - Remove unnecessary `bind`s 92 | - Remove now-unnecessary onWillSave prettifier hack 93 | - Removed code duplication 94 | - Update license information 95 | - Hide MessageObject from UPI 96 | - Do not create error/warning/lint tabs if not using builtin frontend 97 | - Do not show ide-haskell panel when switchTabOnCheck if no messages 98 | - Migrate to Linter V2 99 | - Migrate to new typings 100 | 101 | ## 2.2.0 102 | 103 | - Dispatch appropriate settings on editor's root scope (refer to 104 | [ide-haskell 105 | documentation](https://atom-haskell.github.io/core-packages/ide-haskell/#advanced-configuration-since-v2-2-0) 106 | for more information) 107 | - Individual prettify-on-save toggles **possibly breaking defaults if 108 | using prettify-on-save, check ide-haskell settings** (\#216) 109 | - Move ide-haskell class mark to separate controller; Mark all full 110 | language-haskell grammars with ide-haskell class 111 | - More strict grammar match for editorControl 112 | - Enable checkResults gutter on all full language-haskell grammars 113 | - Simplify tab item count (now ignores URI filter) 114 | 115 | ## 2.1.1 116 | 117 | - Rework outputPanel, defer initialization until package is active 118 | (fixes problems introduced in 2.1.0 by panel deserialization, which 119 | happens before package activation) 120 | - Avoid bind(this) 121 | - Search all panes on item position click 122 | 123 | ## 2.1.0 124 | 125 | - Serialize panel position 126 | - Don't hide panel if it's not in dock 127 | - Remove extraneous commas from setting descriptions 128 | 129 | ## 2.0.5 130 | 131 | - Await package activation before trying to get configParams 132 | - Enforce code style 133 | 134 | ## 2.0.4 135 | 136 | - Handle exceptions in imperative tooltips 137 | - Rewrite UPI2 as a thin adapter to UPI3 138 | - Freeze UPI2 types in def.d.ts 139 | - Better typings. 140 | 141 | ## 2.0.3 142 | 143 | - Avoid duplicate prettifier messages 144 | - Resave after prettify 145 | - Change titles of prettifier-related settings 146 | - Use typings-provided IEventDesc 147 | - Typed emitter 148 | - bump submodules 149 | 150 | ## 2.0.2 151 | 152 | - Fix \#210 153 | - Update typings 154 | 155 | ## 2.0.1 156 | 157 | - Fix prettify-on-save 158 | 159 | ## 2.0.0 160 | 161 | - New internal architecture (hopefully more robust) 162 | - Added statusbar icon to show status & to hide/show panel 163 | - Minimal Atom version required is 1.19 164 | - Panel uses Atom docks 165 | - Dropped space-pen dependency (using 166 | [atom-select-list](https://github.com/atom/atom-select-list) 167 | instead) 168 | - Whole thing rewritten in TypeScript 169 | - Using [etch](https://github.com/atom/etch/) for UI 170 | - Separate statuses for plugins 171 | - UPI 0.3 172 | - Universal atom-linter support 173 | - Autocomplete hint toolitps support 174 | 175 | ## 1.9.6 176 | 177 | - Use Atom tooltips for panel elements 178 | - Fix \#197 179 | 180 | ## 1.9.5 181 | 182 | - Fix ParamSelectView filter key error 183 | 184 | ## 1.9.4 185 | 186 | - Fix \#192 187 | - Add IRC chat info 188 | - Removed gitter badge 189 | 190 | ## 1.9.3 191 | 192 | - Use atom-highlight package for highlighting 193 | 194 | ## 1.9.2 195 | 196 | - Don't depend on marker internals 197 | 198 | ## 1.9.1 199 | 200 | - Make vertical padding smaller on ide-haskell-item-description 201 | - Add README info on changing panel style 202 | 203 | ## 1.9.0 204 | 205 | - Panel tooltips 206 | - Fix state-saved tab activation 207 | - Auto hide output option (thanks @supermario, \#185) 208 | - Vertical panel heading when docked left/right, style cleanup 209 | 210 | ## 1.8.3 211 | 212 | - s/target/currentTarget 213 | - Bring highlighter in-line with other projects 214 | - Atom 1.13 update 215 | - Update CHANGELOG 216 | - Fix LICENSE date 217 | - Update LICENSE 218 | 219 | ## 1.8.2 220 | 221 | - Prepare 1.8.2 release 222 | - Don't emit should-show-tooltip if range under cursor is unchanged 223 | - Use Atom's highlights code for tooltips, etc 224 | 225 | ## 1.8.1 226 | 227 | - Add custom prettifier options 228 | 229 | ## 1.8.0 230 | 231 | - UPI 0.2: get/setConfigParam only promises 232 | 233 | ## 1.7.2 234 | 235 | - Throw error if package isn't active 236 | - Add aspv to deps 237 | 238 | ## 1.7.1 239 | 240 | - Possibly undefined argument 241 | 242 | ## 1.7.0 243 | 244 | - UPI 0.1.0: keep per-project plugin configuration 245 | - Cleanup result-item destruction 246 | - Requires cleanup 247 | - Remove unneeded require 248 | 249 | ## 1.6.7 250 | 251 | - Destroy check-result markers on invalidation 252 | 253 | ## 1.6.6 254 | 255 | - Quick-patch tootlips to work on atom-1.9.0-beta2 256 | - Readme update 257 | 258 | ## 1.6.5 259 | 260 | - Use typeof instead instanceof where possible 261 | 262 | ## 1.6.4 263 | 264 | - AHS bump 265 | - AHS bump 266 | 267 | ## 1.6.3 268 | 269 | - Handle non-zero exit code in prettify 270 | 271 | ## 1.6.2 272 | 273 | - Use general algo to get root dir for prettify 274 | 275 | ## 1.6.1 276 | 277 | - Shut up deprecation cop 278 | - \[README\] Installing with cabal 279 | - Fix changelog 280 | 281 | ## 1.6.0 282 | 283 | - Fix MessageObject toHtml tokenization 284 | - Move build target select list style to ide-haskell-cabal 285 | - Move ide-haskell-target style to ide-haskell-cabal 286 | - Cleaner tooltip arrow 287 | 288 | ## 1.5.4 289 | 290 | - Fix html entities in messages 291 | 292 | ## 1.5.3 293 | 294 | - Clean panel font styles 295 | 296 | ## 1.5.2 297 | 298 | - Bind check result markers to editor id 299 | - Update UPI docs 300 | 301 | ## 1.5.1 302 | 303 | - onShouldShowTooltip callback can return undef/val 304 | 305 | ## 1.5.0 306 | 307 | - Update CHANGELOG 308 | - Show tooltip on selection range 309 | - Do away with ::shadow selector 310 | - Typo (pull request \#144 from @ggreif) 311 | 312 | ## 1.4.2 313 | 314 | - Vertical spacing for multi-message tooltips 315 | - Support array TooltipMessage 316 | 317 | ## 1.4.1 318 | 319 | - Fix some bugs in MessageObject 320 | 321 | ## 1.4.0 322 | 323 | - Add more detail to setup instructions (pull request \#140 from 324 | 1) 325 | 326 | - Pretty tooltips 327 | 328 | ## 1.3.9 329 | 330 | - Add ide-haskell class to atom-workspace 331 | - Grammar 332 | - Update TODO 333 | 334 | ## 1.3.8 335 | 336 | - Fix getEventType for Atom 1.3.x 337 | - update contributors 338 | 339 | ## 1.3.7 340 | 341 | - Might be no controller on close-tooltip 342 | 343 | ## 1.3.6 344 | 345 | - Fix tooltip fail reporting 346 | 347 | ## 1.3.5 348 | 349 | - Cleanup & Fix deprecation warnings 350 | 351 | ## 1.3.4 352 | 353 | - Disable progress bar animation while invisible 354 | 355 | ## 1.3.3 356 | 357 | - Even more robust tooltip hiding (amend 1.3.2) 358 | 359 | ## 1.3.2 360 | 361 | - More robust tooltip hiding 362 | 363 | ## 1.3.1 364 | 365 | - Drop linter styles 366 | 367 | ## 1.3.0 368 | 369 | - Panel resizing for different positions 370 | - Use simpler view API 371 | - Initial support for setting output panel position 372 | 373 | ## 1.2.0 374 | 375 | - Show cursor position on cursor move (\#120) 376 | 377 | ## 1.1.0 378 | 379 | - Handle controller-specific event disposal in controller dtor 380 | - EditorController.onDidStopChanging 381 | 382 | ## 1.0.0 383 | 384 | - UPI interface 385 | 386 | ## 0.8.0 387 | 388 | - Build backend support 389 | - Output panel revamped 390 | - Add 'show info fallback to type' command/mouseover option 391 | - Support for arbitrary message types 392 | - Config cleaned up 393 | - General code cleanup 394 | - Proper disposal in views, using SubAtom for event listeners 395 | - Filter based on current file 396 | - State save 397 | - Moved command registration to backend consumption 398 | - Moved menu initialization to after backend consumption 399 | - Renamed 'Linter' menu option to 'Lint' 400 | - Activation logic revamped 401 | - Removed autocomplete-haskell startup message 402 | 403 | ## 0.7.2 404 | 405 | - Run `stylish-haskell` and `cabal format` in file directory to allow 406 | for more fine-grained control with `.stylish-haskell.yaml` 407 | 408 | ## 0.7.1 409 | 410 | - Fix auto-switch to tab 411 | 412 | ## 0.7.0 413 | 414 | - Go-to-next/prev error 415 | 416 | ## 0.6.2 417 | 418 | - Pass-through `escape` keybinding for close-tooltip if there are no 419 | tooltips 420 | 421 | ## 0.6.1 422 | 423 | - Make ide-backend commands optional (i.e. check if those exist before 424 | calling) 425 | 426 | ## 0.6.0 427 | 428 | - Optional support for AtomLinter for showing project messages, 429 | support for haskell-ghc-mod 0.8.0 430 | 431 | ## 0.5.14 432 | 433 | - Initial support for literate Haskell 434 | 435 | ## 0.5.13 436 | 437 | - Fixes for Atom 0.209 API change w.r.t. mouse position 438 | - Better error tooltips 439 | 440 | ## 0.5.12 441 | 442 | - Fix \#73 443 | 444 | ## 0.5.11 445 | 446 | - Fix \#67 (trying to get row length beyond last row) 447 | - Initial implementation of insert-import 448 | 449 | ## 0.5.10 450 | 451 | - Don't try to restore cursor positions on prettify if no cursor 452 | 453 | ## 0.5.9 454 | 455 | - Run check and lint in onDidSave 456 | - Limit max panel height to 50% viewport 457 | 458 | ## 0.5.8 459 | 460 | - Fix an error when editor is closed while waiting for an operation to 461 | complete. 462 | 463 | ## 0.5.7 464 | 465 | - Run context menu commands on last mouse position 466 | - Version bump to haskell-ide-backend 467 | - Version bump to backend-helper 468 | - Deactivation cleanup 469 | - Insert import stubs 470 | - CSS hack to catch mouse events only on .scroll-view (specialized to 471 | atom-text-editor\[data-grammar\~="haskell"\]) 472 | - Don't queue more than one type/info request (\#63) 473 | - Check if mouse is still in expression range before showing tooltip 474 | (\#63) 475 | - Cabal format (\#24) 476 | 477 | ## 0.5.6 478 | 479 | - Tooltip behavior updates (\#62): 480 | - Don't hide tooltip unless new is ready, or none is expected 481 | - Show tooltip at the start of expression, and not at mouse 482 | position (only when invoked by mouse) 483 | - Set pointer-events:none on atom-overlay 484 | - Disable fade-in to reduce flicker 485 | 486 | ## 0.5.5 487 | 488 | - Show warning state in outputView on fail to get info/type 489 | - Bump to haskell-ide-backend-0.1.1 490 | 491 | ## 0.5.4 492 | 493 | - Preserve cursor position on prettify (\#58) 494 | 495 | ## 0.5.3 496 | 497 | - Make closeTooltipsOnCursorMove matter 498 | - More accurate fix for error on close (\#56) 499 | 500 | ## 0.5.2 501 | 502 | - Fix error on file close (\#56) 503 | 504 | ## 0.5.1 505 | 506 | - Fix error when hovering mouse over selection 507 | 508 | ## 0.5.0 509 | 510 | - Specify Atom version according to docs 511 | - Migration to haskell-ide-backend service 512 | - Autocompletion delegated to autocomplete-haskell 513 | - Stop backend menu option 514 | - Hotkeys configurable from settings window 515 | - Most commands are bound to haskell grammar editors 516 | - Option to prettify file on save (some problems exist) 517 | - Command to insert type 518 | - Now works on standalone Haskell files! 519 | 520 | ## 0.4.2 521 | 522 | - Allowing text selection in bottom pane (Daniel Beskin) 523 | - Fixing a missing resize cursor on Windows (Daniel Beskin) 524 | 525 | ## 0.4.1 526 | 527 | - Somewhat better error-reporting on ghc-mod errors 528 | - Options descriptions 529 | 530 | ## 0.4.0 531 | 532 | - Fixed main file deprecations 533 | - Fixed \#50 534 | 535 | ## 0.3.6 536 | 537 | - Fixed \#48 538 | 539 | ## 0.3.5 540 | 541 | - Fixed ghc-mod newline compatibility on Windows (Luka Horvat) 542 | 543 | ## 0.3.4 544 | 545 | - Fixed \#44 546 | 547 | ## 0.3.3 548 | 549 | - Fixed \#26, \#37, \#40 550 | - Added a hack, which should hopefully fix \#29 551 | 552 | ## 0.3.2 553 | 554 | - Fixed \#16 555 | - Fixed \#25 556 | 557 | ## 0.3.1 558 | 559 | - Fixed: Upgrade to atom 1.0 api. Upgrade autocomplete (John Quigley) 560 | - Fixed: Fix issue requiring package to be manually 561 | deactivated/reactivated (John Quigley) 562 | 563 | ## 0.3.0 564 | 565 | - New: Code prettify by `stylish-haskell`. 566 | 567 | ## 0.2.0 568 | 569 | - New: Autocompletion feature added (issue \#4). 570 | - Fixed: Types and errors tooltips were not showed if `Soft Wrap` was 571 | turned on. 572 | 573 | ## 0.1.2 574 | 575 | - Fixed: \#3, \#8, \#9, \#10, \#11, \#13. 576 | - Fixed: After multiple package enable-disable actions multiple 577 | `ghc-mod` utilities started in concurrent with the same task. 578 | 579 | ## 0.1.1 580 | 581 | - Fixed: Package disable and uninstall works now. 582 | 583 | ## 0.1.0 584 | 585 | - First release. 586 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Alexander Chaika 2 | Copyright (c) 2015 Atom-Haskell 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | IcoMoon font located in resources/icomoon.woff is created by 24 | @Keyamoon (https://github.com/Keyamoon) and is licensed by GPLv3 or CC BY 4.0. 25 | 26 | See 27 | 28 | * http://creativecommons.org/licenses/by/4.0/ 29 | * http://www.gnu.org/licenses/gpl.html 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IDE-Haskell [![](https://david-dm.org/atom-haskell/ide-haskell.svg)](https://david-dm.org/atom-haskell/ide-haskell) [![](https://travis-ci.org/atom-haskell/ide-haskell.svg?branch=master)](https://travis-ci.org/atom-haskell/ide-haskell) 2 | 3 | Welcome to IDE-Haskell plugin for amazing [Atom](http://atom.io) editor! This 4 | plugin is intended to help you with development in 5 | [Haskell](http://haskell.org). 6 | 7 | **NOTE:** You must install dependencies in addition to 8 | installing the package itself. Refer to documentation site https://atom-haskell.github.io/ for setup and usage instructions. 9 | 10 | ## Features 11 | 12 | Here is a brief and incomplete overview. Visit https://atom-haskell.github.io/ for more details. 13 | 14 | #### Errors, warnings and linter 15 | 16 | ![errors](https://cloud.githubusercontent.com/assets/7275622/9705079/52b38f7c-54c1-11e5-9b23-6b932100e876.gif) 17 | 18 | #### Get type/info 19 | 20 | ![typeinfo](https://cloud.githubusercontent.com/assets/7275622/9705082/52daa81e-54c1-11e5-88a8-99c8029eb14e.gif) 21 | 22 | #### Insert type 23 | 24 | ![typeins](https://cloud.githubusercontent.com/assets/7275622/9705080/52cd7e64-54c1-11e5-8ee3-120641da2f85.gif) 25 | 26 | #### Code prettify/format 27 | 28 | ![prettify](https://cloud.githubusercontent.com/assets/7275622/9705081/52d97cf0-54c1-11e5-94f0-96f09e43ada3.gif) 29 | 30 | #### Build and test project 31 | 32 | If you have `ide-haskell-cabal` or similar package installed, you can build, 33 | clean and test your project from ide-haskell (stack and cabal supported) 34 | 35 | #### Autocompletion 36 | 37 | ![autocompletion](https://cloud.githubusercontent.com/assets/7275622/9704861/e4474ec4-54bc-11e5-92f4-84a3995e45cb.gif) 38 | 39 | ## API 40 | 41 | Ide-haskell provides service-hub API with `ide-haskell-upi` service. 42 | 43 | More information is available in [lib/upi.coffee][upi] source file 44 | 45 | [upi]: https://github.com/atom-haskell/ide-haskell/blob/master/lib/upi.coffee 46 | 47 | ## TODO 48 | 49 | - [x] Cabal project autodetection (via language-haskell) 50 | - [x] Errors, warnings and linter (via haskell-ghc-mod) 51 | - [x] Get type at point (via haskell-ghc-mod) 52 | - [x] Autocompletion (via haskell-ghc-mod and autocomplete-haskell) 53 | - [x] Code beautify 54 | - [x] Cabal project management (with ide-haskell-cabal) 55 | - [x] Jump to definition (since haskell-ghc-mod 1.3.0, or with ide-haskell-hasktags) 56 | - [x] Interactive REPL (with ide-haskell-repl) 57 | - [x] Stack project management (with ide-haskell-cabal) 58 | - [ ] Who calls and vice versa 59 | - [x] Documentation support (alpha, with ide-haskell-hoogle) 60 | 61 | ## Changelog 62 | 63 | Changelog is available [here][CHANGELOG]. 64 | 65 | ## License 66 | 67 | Copyright © 2014 Alexander Chaika \ 68 | Copyright © 2015 Atom-Haskell 69 | 70 | Contributors (by number of commits): 71 | 72 | 73 | * Nikolay Yakimov 74 | * Alexander Chaika 75 | * John Quigley 76 | * Ondřej Janošík 77 | * Luka Horvat 78 | * Gabriel Gonzalez 79 | * Daniel Beskin 80 | * Gabor Greif 81 | * Daniel Gröber 82 | 83 | 84 | 85 | See the [LICENSE.md][LICENSE] for details. 86 | 87 | [CHANGELOG]: https://github.com/atom-haskell/ide-haskell/blob/master/CHANGELOG.md 88 | [LICENSE]: https://github.com/atom-haskell/ide-haskell/blob/master/LICENSE.md 89 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrow_spacing": { 3 | "name": "arrow_spacing", 4 | "level": "error" 5 | }, 6 | "ensure_comprehensions": { 7 | "name": "ensure_comprehensions", 8 | "level": "error" 9 | }, 10 | "max_line_length": { 11 | "name": "max_line_length", 12 | "value": 120, 13 | "level": "error", 14 | "limitComments": true 15 | }, 16 | "indentation": { 17 | "name": "indentation", 18 | "value": 2, 19 | "level": "error" 20 | }, 21 | "no_empty_param_list": { 22 | "name": "no_empty_param_list", 23 | "level": "error" 24 | }, 25 | "cyclomatic_complexity": { 26 | "name": "cyclomatic_complexity", 27 | "value": 22, 28 | "level": "error" 29 | }, 30 | "no_unnecessary_fat_arrows": { 31 | "name": "no_unnecessary_fat_arrows", 32 | "level": "error" 33 | }, 34 | "space_operators": { 35 | "name": "space_operators", 36 | "level": "error" 37 | }, 38 | "spacing_after_comma": { 39 | "name": "spacing_after_comma", 40 | "level": "error" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /keymaps/ide-haskell.cson: -------------------------------------------------------------------------------- 1 | "atom-text-editor.ide-haskell--has-tooltips": 2 | "escape": "ide-haskell:close-tooltip" 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ide-haskell", 3 | "main": "./dist/main.js", 4 | "version": "2.7.0", 5 | "description": "Haskell IDE", 6 | "keywords": [ 7 | "ide-haskell", 8 | "ide", 9 | "haskell" 10 | ], 11 | "repository": "https://github.com/atom-haskell/ide-haskell", 12 | "license": "MIT", 13 | "activationHooks": [ 14 | "language-haskell:grammar-used" 15 | ], 16 | "engines": { 17 | "atom": ">=1.46.0 <2.0.0" 18 | }, 19 | "scripts": { 20 | "build": "tsc --project . && parcel build lib/ide-haskell.js", 21 | "prettier": "prettier --write 'src/**/*.ts?(x)' 'spec/**/*.ts?(x)'", 22 | "prettier-check": "prettier -l 'src/**/*.ts' 'spec/**/*.ts'", 23 | "typecheck": "tsc --noEmit -p . && tsc --noEmit -p spec", 24 | "lint": "tslint --project . && tslint --project spec", 25 | "test": "npm run typecheck && npm run lint && npm run prettier-check" 26 | }, 27 | "atomTestRunner": "./node_modules/atom-ts-spec-runner/runner.js", 28 | "providedServices": { 29 | "ide-haskell-upi": { 30 | "description": "Universal pluggable interface", 31 | "versions": { 32 | "0.3.0": "provideUpi3_0", 33 | "0.3.1": "provideUpi3_1", 34 | "0.3.2": "provideUpi3_2", 35 | "0.3.3": "provideUpi3_3" 36 | } 37 | } 38 | }, 39 | "consumedServices": { 40 | "linter-indie": { 41 | "versions": { 42 | "2.0.0": "consumeLinter" 43 | } 44 | }, 45 | "status-bar": { 46 | "versions": { 47 | "^1.0.0": "consumeStatusBar" 48 | } 49 | }, 50 | "ide-haskell-upi-plugin": { 51 | "versions": { 52 | "0.3.0": "consumeUpi3_0", 53 | "0.3.1": "consumeUpi3_1", 54 | "0.3.2": "consumeUpi3_2" 55 | } 56 | } 57 | }, 58 | "dependencies": { 59 | "atom-haskell-utils": "^1.0.2" 60 | }, 61 | "devDependencies": { 62 | "@types/atom": "^1.40.10", 63 | "@types/chai": "^4.2.16", 64 | "@types/mocha": "^8.2.2", 65 | "@types/node": "^12", 66 | "@types/temp": "^0.9.0", 67 | "atom-haskell-tslint-rules": "^0.2.2", 68 | "atom-highlight": "^0.5.0", 69 | "atom-select-list": "^0.8.0", 70 | "atom-ts-spec-runner": "^1.1.1", 71 | "chai": "^4.3.4", 72 | "etch": "^0.14.1", 73 | "lodash": "^4.17.21", 74 | "lodash-decorators": "^6.0.1", 75 | "mocha": "^8.3.2", 76 | "parcel": "2.0.0-beta.1", 77 | "prettier": "^2.2.1", 78 | "temp": "^0.9.4", 79 | "ts-node": "^9.1.1", 80 | "tslib": "^2.2.0", 81 | "tslint": "^6.1.2", 82 | "typedoc": "^0.20.35", 83 | "typescript": "~4.2.4", 84 | "typescript-tslint-plugin": "^1.0.1" 85 | }, 86 | "targets": { 87 | "main": { 88 | "context": "electron-renderer", 89 | "includeNodeModules": { 90 | "atom": false, 91 | "atom-haskell-utils": false 92 | }, 93 | "outputFormat": "commonjs", 94 | "isLibrary": true 95 | } 96 | }, 97 | "deserializers": { 98 | "ide-haskell/OutputPanel": "deserializeOutputPanel" 99 | }, 100 | "configSchema": { 101 | "onSavePrettify": { 102 | "type": "boolean", 103 | "default": false, 104 | "description": "Run file through prettifier before save", 105 | "order": 20 106 | }, 107 | "onSavePrettifyFormats": { 108 | "type": "object", 109 | "title": "Formats to prettify on save", 110 | "order": 21, 111 | "properties": { 112 | "source*c2hs": { 113 | "type": "boolean", 114 | "default": false, 115 | "title": "C2HS", 116 | "order": 40 117 | }, 118 | "source*cabal": { 119 | "type": "boolean", 120 | "default": false, 121 | "title": "Cabal files", 122 | "description": "Unlike others, will use `cabal format`", 123 | "order": 20 124 | }, 125 | "source*hsc2hs": { 126 | "type": "boolean", 127 | "default": false, 128 | "title": "HSC2HS", 129 | "order": 50 130 | }, 131 | "source*haskell": { 132 | "type": "boolean", 133 | "default": true, 134 | "title": "Haskell", 135 | "order": 10 136 | }, 137 | "text*tex*latex*haskell": { 138 | "type": "boolean", 139 | "default": false, 140 | "title": "Literate Haskell", 141 | "order": 15 142 | }, 143 | "source*hsig": { 144 | "type": "boolean", 145 | "default": false, 146 | "title": "Module signatures (hsig)", 147 | "order": 30 148 | } 149 | } 150 | }, 151 | "switchTabOnCheck": { 152 | "type": "boolean", 153 | "default": true, 154 | "description": "Automatically switch to leftmost updated tab (error/warning/lint/etc)", 155 | "order": 10 156 | }, 157 | "switchTabOnCheckInterval": { 158 | "type": "integer", 159 | "default": 300, 160 | "description": "Messages grouping time interval in ms, raise this if switchTabOnCheck switches to wrong tabs", 161 | "order": 11 162 | }, 163 | "expressionTypeInterval": { 164 | "type": "integer", 165 | "default": 300, 166 | "description": "Type/Info tooltip show delay, in ms", 167 | "order": 30 168 | }, 169 | "onCursorMove": { 170 | "type": "string", 171 | "description": "Show check results (error, lint) description tooltips when text cursor is near marker, close open tooltips, or do nothing?", 172 | "enum": [ 173 | "Show Tooltip", 174 | "Hide Tooltip", 175 | "Nothing" 176 | ], 177 | "default": "Nothing", 178 | "order": 40 179 | }, 180 | "messageDisplayFrontend": { 181 | "type": "string", 182 | "default": "builtin", 183 | "description": "Frontend to use for displaying errors/warnigns/lints. Builtin (i.e. output panel) and atom-linter supported. Requires Atom restart.", 184 | "enum": [ 185 | "builtin", 186 | "linter" 187 | ], 188 | "order": 45 189 | }, 190 | "stylishHaskellPath": { 191 | "type": "string", 192 | "default": "stylish-haskell", 193 | "title": "Prettifier Path", 194 | "description": "Path to `stylish-haskell` utility or other prettifier", 195 | "order": 60 196 | }, 197 | "stylishHaskellArguments": { 198 | "type": "array", 199 | "default": [], 200 | "title": "Prettifier Arguments", 201 | "description": "Additional arguments to pass to prettifier; comma-separated", 202 | "items": { 203 | "type": "string" 204 | }, 205 | "order": 70 206 | }, 207 | "cabalPath": { 208 | "type": "string", 209 | "default": "cabal", 210 | "description": "Path to `cabal` utility, for `cabal format`", 211 | "order": 50 212 | }, 213 | "startupMessageIdeBackend": { 214 | "type": "boolean", 215 | "default": true, 216 | "description": "Show info message about haskell-ide-backend service on activation", 217 | "order": 80 218 | }, 219 | "panelPosition": { 220 | "type": "string", 221 | "default": "bottom", 222 | "title": "Default Panel Position", 223 | "description": "Default output panel position", 224 | "enum": [ 225 | "bottom", 226 | "left", 227 | "right", 228 | "center" 229 | ], 230 | "order": 41 231 | }, 232 | "buttonsPosition": { 233 | "type": "string", 234 | "default": "top", 235 | "title": "Panel buttons position", 236 | "enum": [ 237 | "top", 238 | "left" 239 | ], 240 | "order": 42 241 | }, 242 | "hideParameterValues": { 243 | "type": "boolean", 244 | "default": false, 245 | "description": "Hide additional plugin parameter values until hovered", 246 | "order": 12 247 | }, 248 | "autoHideOutput": { 249 | "type": "boolean", 250 | "default": false, 251 | "description": "Hide panel output when there are no new messages to show", 252 | "order": 11 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /resources/haskell.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 55 | 59 | 63 | 64 | -------------------------------------------------------------------------------- /resources/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atom-haskell/ide-haskell/b22229da1606a3bd2ce1b3d7917c345c7acb8a3f/resources/icomoon.woff -------------------------------------------------------------------------------- /spec/package.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { join } from 'path' 3 | 4 | const pkg = join(__dirname, '..') 5 | 6 | describe('package', function() { 7 | this.timeout(60000) 8 | it('should activate', async () => { 9 | const packages = atom.packages 10 | 11 | // Load package, but it won't activate until the grammar is used 12 | const promise = atom.packages.activatePackage(pkg) 13 | 14 | packages.triggerActivationHook('language-haskell:grammar-used') 15 | packages.triggerDeferredActivationHooks() 16 | 17 | await promise 18 | 19 | expect(atom.packages.isPackageActive('ide-haskell')).to.equal(true) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /spec/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs" 5 | }, 6 | "compileOnSave": false, 7 | "extends": "../tsconfig.json", 8 | "include": ["./**.ts"], 9 | "exclude": [] 10 | } 11 | -------------------------------------------------------------------------------- /spec/tslint.json: -------------------------------------------------------------------------------- 1 | { "extends": "../tslint.json" } 2 | -------------------------------------------------------------------------------- /src/actions-registry/index.ts: -------------------------------------------------------------------------------- 1 | import { TextEditor, Range, CompositeDisposable } from 'atom' 2 | import { PluginManager } from '../plugin-manager' 3 | import * as UPI from 'atom-haskell-upi' 4 | import TEventRangeType = UPI.TEventRangeType 5 | import { PriorityRegistry } from '../priority-registry' 6 | import { selectAction } from './select-action' 7 | 8 | export class ActionRegistry extends PriorityRegistry { 9 | protected disposables = new CompositeDisposable() 10 | constructor(private readonly pluginManager: PluginManager) { 11 | super() 12 | this.disposables.add( 13 | atom.commands.add('atom-text-editor.ide-haskell', { 14 | 'ide-haskell:show-actions': async ({ currentTarget }) => { 15 | const editor = currentTarget.getModel() 16 | const act = await this.pluginManager.actionRegistry.getActions( 17 | editor, 18 | TEventRangeType.context, // context is used to force value even on empty range 19 | editor.getSelectedBufferRange(), 20 | ) 21 | if (act && act.length) { 22 | const choice = await selectAction(act) 23 | if (choice) await choice.apply() 24 | } 25 | }, 26 | }), 27 | ) 28 | } 29 | 30 | public dispose() { 31 | super.dispose() 32 | this.disposables.dispose() 33 | } 34 | 35 | public async getActions( 36 | editor: TextEditor, 37 | type: TEventRangeType, 38 | crange: Range, 39 | ) { 40 | for (const { pluginName, handler, eventTypes } of this.providers) { 41 | if (!eventTypes.includes(type)) { 42 | continue 43 | } 44 | const awaiter = this.pluginManager.getAwaiter(pluginName) 45 | const actions = await awaiter(() => handler(editor, crange, type)) 46 | if (actions === undefined) continue 47 | if (!actions.length) continue 48 | return actions 49 | } 50 | return undefined 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/actions-registry/select-action.ts: -------------------------------------------------------------------------------- 1 | import { Panel } from 'atom' 2 | import * as UPI from 'atom-haskell-upi' 3 | import SelectListView from 'atom-select-list' 4 | export async function selectAction(actions: UPI.Action[]) { 5 | let panel: Panel | undefined 6 | const currentFocus = document.activeElement as HTMLElement | undefined | null 7 | try { 8 | return await new Promise((resolve) => { 9 | const select = new SelectListView({ 10 | items: actions, 11 | infoMessage: 'Actions', 12 | itemsClassList: ['ide-haskell', 'mark-active'], 13 | elementForItem: (x) => { 14 | const el = document.createElement('li') 15 | el.innerText = x.title 16 | return el 17 | }, 18 | filterKeyForItem: (x) => x.title, 19 | didCancelSelection: () => { 20 | resolve(undefined) 21 | }, 22 | didConfirmSelection: (item) => { 23 | resolve(item) 24 | }, 25 | }) 26 | select.element.classList.add('ide-haskell') 27 | panel = atom.workspace.addModalPanel({ 28 | item: select, 29 | visible: true, 30 | }) 31 | select.focus() 32 | }) 33 | } finally { 34 | panel?.destroy() 35 | currentFocus?.focus() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/atom-config.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | // generated by typed-config.js 3 | declare module 'atom' { 4 | interface ConfigValues { 5 | 'ide-haskell.onSavePrettify': boolean 6 | 'ide-haskell.onSavePrettifyFormats.source*c2hs': boolean 7 | 'ide-haskell.onSavePrettifyFormats.source*cabal': boolean 8 | 'ide-haskell.onSavePrettifyFormats.source*hsc2hs': boolean 9 | 'ide-haskell.onSavePrettifyFormats.source*haskell': boolean 10 | 'ide-haskell.onSavePrettifyFormats.text*tex*latex*haskell': boolean 11 | 'ide-haskell.onSavePrettifyFormats.source*hsig': boolean 12 | 'ide-haskell.onSavePrettifyFormats': { 13 | 'source*c2hs': boolean 14 | 'source*cabal': boolean 15 | 'source*hsc2hs': boolean 16 | 'source*haskell': boolean 17 | 'text*tex*latex*haskell': boolean 18 | 'source*hsig': boolean 19 | } 20 | 'ide-haskell.switchTabOnCheck': boolean 21 | 'ide-haskell.switchTabOnCheckInterval': number 22 | 'ide-haskell.expressionTypeInterval': number 23 | 'ide-haskell.onCursorMove': 'Show Tooltip' | 'Hide Tooltip' | 'Nothing' 24 | 'ide-haskell.messageDisplayFrontend': 'builtin' | 'linter' 25 | 'ide-haskell.stylishHaskellPath': string 26 | 'ide-haskell.stylishHaskellArguments': string[] 27 | 'ide-haskell.cabalPath': string 28 | 'ide-haskell.startupMessageIdeBackend': boolean 29 | 'ide-haskell.panelPosition': 'bottom' | 'left' | 'right' | 'center' 30 | 'ide-haskell.buttonsPosition': 'top' | 'left' 31 | 'ide-haskell.hideParameterValues': boolean 32 | 'ide-haskell.autoHideOutput': boolean 33 | 'ide-haskell': { 34 | onSavePrettify: boolean 35 | 'onSavePrettifyFormats.source*c2hs': boolean 36 | 'onSavePrettifyFormats.source*cabal': boolean 37 | 'onSavePrettifyFormats.source*hsc2hs': boolean 38 | 'onSavePrettifyFormats.source*haskell': boolean 39 | 'onSavePrettifyFormats.text*tex*latex*haskell': boolean 40 | 'onSavePrettifyFormats.source*hsig': boolean 41 | onSavePrettifyFormats: { 42 | 'source*c2hs': boolean 43 | 'source*cabal': boolean 44 | 'source*hsc2hs': boolean 45 | 'source*haskell': boolean 46 | 'text*tex*latex*haskell': boolean 47 | 'source*hsig': boolean 48 | } 49 | switchTabOnCheck: boolean 50 | switchTabOnCheckInterval: number 51 | expressionTypeInterval: number 52 | onCursorMove: 'Show Tooltip' | 'Hide Tooltip' | 'Nothing' 53 | messageDisplayFrontend: 'builtin' | 'linter' 54 | stylishHaskellPath: string 55 | stylishHaskellArguments: string[] 56 | cabalPath: string 57 | startupMessageIdeBackend: boolean 58 | panelPosition: 'bottom' | 'left' | 'right' | 'center' 59 | buttonsPosition: 'top' | 'left' 60 | hideParameterValues: boolean 61 | autoHideOutput: boolean 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/atom.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | declare module 'atom' { 3 | interface CommandRegistryTargetMap { 4 | 'atom-text-editor.ide-haskell': TextEditorElement 5 | 'atom-text-editor.ide-haskell--has-tooltips': TextEditorElement 6 | } 7 | interface TextEditor { 8 | getLastCursor(): Cursor | undefined // TODO: Upstream to DT 9 | } 10 | } 11 | declare global { 12 | interface ArrayConstructor { 13 | isArray(arg: ReadonlyArray | any): arg is ReadonlyArray 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/backend-status/index.tsx: -------------------------------------------------------------------------------- 1 | import * as etch from 'etch' 2 | import * as UPI from 'atom-haskell-upi' 3 | import { Emitter, Disposable } from 'atom' 4 | import { StatusIcon } from './views/status-icon' 5 | import { ProgressBar } from './views/progress-bar' 6 | 7 | export class BackendStatusController { 8 | private readonly statusMap: Map = new Map() 9 | private readonly awaiters: Map>> = new Map() 10 | private readonly emitter = new Emitter< 11 | {}, 12 | { 13 | 'did-update': { pluginName: string; status: UPI.IStatus } 14 | } 15 | >() 16 | 17 | public getAwaiter(pluginName: string) { 18 | let activeActionsVar = this.awaiters.get(pluginName) 19 | if (activeActionsVar === undefined) { 20 | activeActionsVar = new Set() 21 | this.awaiters.set(pluginName, activeActionsVar) 22 | } 23 | const activeActions = activeActionsVar 24 | return async (action: () => Promise | T): Promise => { 25 | let promise 26 | try { 27 | promise = Promise.resolve().then(action) 28 | activeActions.add(promise) 29 | this.updateStatus(pluginName, { status: 'progress', detail: '' }) 30 | const res = await promise 31 | activeActions.delete(promise) 32 | if (activeActions.size === 0) { 33 | this.updateStatus(pluginName, { status: 'ready', detail: '' }) 34 | } 35 | return res 36 | } catch (e) { 37 | if (promise) activeActions.delete(promise) 38 | this.updateStatus(pluginName, { status: 'warning', detail: `${e}` }) 39 | console.warn(e) 40 | return undefined 41 | } 42 | } 43 | } 44 | 45 | public forceBackendStatus(pluginName: string, status: UPI.IStatus) { 46 | this.updateStatus(pluginName, status) 47 | } 48 | 49 | public onDidUpdate( 50 | callback: (value: { pluginName: string; status: UPI.IStatus }) => void, 51 | ): Disposable { 52 | return this.emitter.on('did-update', callback) 53 | } 54 | 55 | public renderStatusIcon() { 56 | return 57 | } 58 | 59 | public renderProgressBar() { 60 | const progress = Array.from(this.statusMap.values()).reduce((cv, i) => { 61 | if (i.status === 'progress' && i.progress !== undefined) { 62 | cv.push(i.progress) 63 | } 64 | return cv 65 | }, [] as number[]) 66 | return 67 | } 68 | 69 | private updateStatus(pluginName: string, status: UPI.IStatus) { 70 | this.statusMap.set(pluginName, status) 71 | this.emitter.emit('did-update', { pluginName, status }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/backend-status/views/progress-bar.tsx: -------------------------------------------------------------------------------- 1 | import * as etch from 'etch' 2 | 3 | export interface IProps extends JSX.Props { 4 | progress: number[] 5 | } 6 | 7 | type ElementClass = JSX.ElementClass 8 | 9 | export class ProgressBar implements ElementClass { 10 | constructor(public props: IProps) { 11 | etch.initialize(this) 12 | } 13 | 14 | public render() { 15 | const progress = this.aveProgress() 16 | if (isNaN(progress)) return 17 | else if (progress === 0) return 18 | else return 19 | } 20 | 21 | public async update(props: IProps) { 22 | if (this.props.progress !== props.progress) { 23 | this.props.progress = props.progress 24 | return etch.update(this) 25 | } else { 26 | return Promise.resolve() 27 | } 28 | } 29 | 30 | public async destroy() { 31 | await etch.destroy(this) 32 | } 33 | 34 | private aveProgress() { 35 | return ( 36 | this.props.progress.reduce((a, b) => a + b, 0) / 37 | this.props.progress.length 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/backend-status/views/status-icon.tsx: -------------------------------------------------------------------------------- 1 | import * as etch from 'etch' 2 | import { CompositeDisposable } from 'atom' 3 | import * as UPI from 'atom-haskell-upi' 4 | 5 | export interface IProps extends JSX.Props { 6 | statusMap: Map 7 | } 8 | 9 | type ElementClass = JSX.ElementClass 10 | 11 | export class StatusIcon implements ElementClass { 12 | private disposables: CompositeDisposable 13 | private element!: HTMLElement 14 | constructor(public props: IProps) { 15 | this.disposables = new CompositeDisposable() 16 | 17 | etch.initialize(this) 18 | 19 | this.disposables.add( 20 | atom.tooltips.add(this.element, { 21 | class: 'ide-haskell-status-tooltip', 22 | title: () => { 23 | const res = [] 24 | for (const [ 25 | plugin, 26 | { status, detail }, 27 | ] of this.props.statusMap.entries()) { 28 | res.push(` 29 | 30 | ${plugin} 31 | ${ 32 | detail ? detail : '' 33 | } 34 | 35 | `) 36 | } 37 | return res.join('') 38 | }, 39 | }), 40 | ) 41 | } 42 | 43 | public render() { 44 | return ( 45 | 46 | ) 47 | } 48 | 49 | public async update(props: IProps) { 50 | // TODO: Diff algo 51 | this.props.statusMap = props.statusMap 52 | return etch.update(this) 53 | } 54 | 55 | public async destroy() { 56 | await etch.destroy(this) 57 | this.props.statusMap.clear() 58 | } 59 | 60 | private calcCurrentStatus(): 'ready' | 'warning' | 'error' | 'progress' { 61 | const prio = { 62 | progress: 50, 63 | error: 20, 64 | warning: 10, 65 | ready: 0, 66 | } 67 | const stArr = Array.from(this.props.statusMap.values()) 68 | if (stArr.length === 0) return 'ready' 69 | const [consensus] = stArr.sort((a, b) => prio[b.status] - prio[a.status]) 70 | return consensus.status 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/check-results-provider/editor-control.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Range, 3 | TextEditor, 4 | Point, 5 | CompositeDisposable, 6 | Gutter, 7 | DisplayMarker, 8 | DisplayMarkerLayer, 9 | } from 'atom' 10 | import * as UPI from 'atom-haskell-upi' 11 | import TEventRangeType = UPI.TEventRangeType 12 | 13 | import { ResultsDB, ResultItem } from '../results-db' 14 | import { PluginManager, IEditorController } from '../plugin-manager' 15 | import { 16 | listen, 17 | bufferPositionFromMouseEvent, 18 | handlePromise, 19 | MessageObject, 20 | } from '../utils' 21 | import { TooltipManager } from '../tooltip-manager' 22 | 23 | export class CREditorControl implements IEditorController { 24 | private gutter: Gutter 25 | private gutterElement: HTMLElement 26 | private markers: DisplayMarkerLayer 27 | private disposables: CompositeDisposable 28 | private markerProps: WeakMap 29 | private tooltipManager: TooltipManager 30 | private resultsDB: ResultsDB 31 | constructor(private editor: TextEditor, pluginManager: PluginManager) { 32 | const gutter = this.editor.gutterWithName('ide-haskell-check-results') 33 | if (gutter) { 34 | this.gutter = gutter 35 | } else { 36 | this.gutter = this.editor.addGutter({ 37 | name: 'ide-haskell-check-results', 38 | priority: 10, 39 | }) 40 | } 41 | this.gutterElement = atom.views.getView(this.gutter) 42 | 43 | this.resultsDB = pluginManager.resultsDB 44 | this.tooltipManager = pluginManager.tooltipManager 45 | 46 | this.disposables = new CompositeDisposable() 47 | this.markers = editor.addMarkerLayer({ 48 | maintainHistory: true, 49 | persistent: false, 50 | }) 51 | this.markerProps = new WeakMap() 52 | this.disposables.add(this.resultsDB.onDidUpdate(this.updateResults)) 53 | this.updateResults() 54 | this.registerGutterEvents() 55 | } 56 | 57 | public static supportsGrammar(grammar: string): boolean { 58 | return [ 59 | 'source.c2hs', 60 | // 'source.cabal', 61 | 'source.hsc2hs', 62 | 'source.haskell', 63 | 'text.tex.latex.haskell', 64 | 'source.hsig', 65 | ].includes(grammar) 66 | } 67 | 68 | public destroy() { 69 | this.markers.destroy() 70 | this.disposables.dispose() 71 | try { 72 | this.gutter.destroy() 73 | } catch (e) { 74 | // tslint:disable-next-line:no-console 75 | console.warn(e) 76 | } 77 | } 78 | 79 | public getMessageAt( 80 | pos: Point, 81 | type: TEventRangeType | 'gutter', 82 | ): MessageObject[] { 83 | return Array.from(this.getResultAt(pos, type)).map((res) => res.message) 84 | } 85 | 86 | private *getResultAt(pos: Point, type: TEventRangeType | 'gutter') { 87 | const markers = this.find(pos, type) 88 | for (const marker of markers) { 89 | if (!marker.isValid()) continue 90 | const res = this.markerProps.get(marker) 91 | if (!res) continue 92 | yield res 93 | } 94 | } 95 | 96 | private registerGutterEvents() { 97 | this.disposables.add( 98 | listen(this.gutterElement, 'mouseover', '.decoration', (e) => { 99 | const bufferPt = bufferPositionFromMouseEvent( 100 | this.editor, 101 | e as MouseEvent, 102 | ) 103 | if (bufferPt) { 104 | const msg = this.getMessageAt(bufferPt, 'gutter') 105 | if (msg.length > 0) { 106 | handlePromise( 107 | this.tooltipManager.showTooltip( 108 | this.editor, 109 | TEventRangeType.mouse, 110 | { 111 | pluginName: 'builtin:check-results', 112 | tooltip: { 113 | text: msg, 114 | range: new Range(bufferPt, bufferPt), 115 | }, 116 | }, 117 | ), 118 | ) 119 | } 120 | } 121 | }), 122 | ) 123 | this.disposables.add( 124 | listen(this.gutterElement, 'mouseout', '.decoration', () => 125 | this.tooltipManager.hideTooltip( 126 | this.editor, 127 | TEventRangeType.mouse, 128 | 'builtin:check-results', 129 | ), 130 | ), 131 | ) 132 | } 133 | 134 | private updateResults = () => { 135 | const path = this.editor.getPath() 136 | const resultsToMark = this.resultsDB.filter( 137 | (m) => m.uri === path && m.isValid(), 138 | ) 139 | const currentMarkers = this.markers.getMarkers() 140 | const newResults = resultsToMark.filter((r) => 141 | currentMarkers.every((m) => this.markerProps.get(m) !== r), 142 | ) 143 | const markersToDelete = currentMarkers.filter((m) => { 144 | const p = this.markerProps.get(m) 145 | return !p || !resultsToMark.includes(p) 146 | }) 147 | markersToDelete.forEach((m) => m.destroy()) 148 | for (const r of newResults) { 149 | this.markerFromCheckResult(r) 150 | } 151 | } 152 | 153 | private markerFromCheckResult(resItem: ResultItem) { 154 | const { position } = resItem 155 | if (!position) { 156 | return 157 | } 158 | 159 | const range = new Range( 160 | position, 161 | Point.fromObject([position.row, position.column + 1]), 162 | ) 163 | const marker = this.markers.markBufferRange(range, { invalidate: 'inside' }) 164 | this.markerProps.set(marker, resItem) 165 | const disp = new CompositeDisposable() 166 | disp.add( 167 | marker.onDidDestroy(() => { 168 | this.markerProps.delete(marker) 169 | disp.dispose() 170 | }), 171 | marker.onDidChange(({ isValid }: { isValid: boolean }) => { 172 | resItem.setValid(isValid) 173 | }), 174 | ) 175 | this.decorateMarker(marker, resItem) 176 | } 177 | 178 | private decorateMarker(m: DisplayMarker, r: ResultItem) { 179 | const cls = { class: `ide-haskell-${r.severity}` } 180 | this.gutter.decorateMarker(m, { type: 'line-number', ...cls }) 181 | this.editor.decorateMarker(m, { type: 'highlight', ...cls }) 182 | } 183 | 184 | private find(pos: Point, type: TEventRangeType | 'gutter') { 185 | switch (type) { 186 | case 'gutter': 187 | return this.markers.findMarkers({ startBufferRow: pos.row }) 188 | case 'keyboard': 189 | return this.markers.findMarkers({ startBufferPosition: pos }) 190 | case 'mouse': 191 | return this.markers.findMarkers({ containsBufferPosition: pos }) 192 | default: 193 | throw new TypeError('Switch assertion failed') 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/check-results-provider/index.ts: -------------------------------------------------------------------------------- 1 | import { Range, TextEditor, CompositeDisposable } from 'atom' 2 | import * as UPI from 'atom-haskell-upi' 3 | import TEventRangeType = UPI.TEventRangeType 4 | 5 | import { PluginManager } from '../plugin-manager' 6 | import { CREditorControl } from './editor-control' 7 | import { ITooltipDataExt } from '../tooltip-registry' 8 | 9 | export class CheckResultsProvider { 10 | private disposables: CompositeDisposable 11 | constructor(private pluginManager: PluginManager) { 12 | const tooltipRegistry = pluginManager.tooltipRegistry 13 | 14 | this.disposables = new CompositeDisposable() 15 | this.disposables.add( 16 | tooltipRegistry.register('builtin:check-results', { 17 | priority: 1000, 18 | handler: this.tooltipProvider, 19 | eventTypes: [TEventRangeType.mouse, TEventRangeType.keyboard], 20 | }), 21 | pluginManager.addEditorController(CREditorControl), 22 | ) 23 | } 24 | 25 | public destroy() { 26 | this.disposables.dispose() 27 | } 28 | 29 | private tooltipProvider = ( 30 | editor: TextEditor, 31 | crange: Range, 32 | type: TEventRangeType, 33 | ): ITooltipDataExt | undefined => { 34 | const controller = this.pluginManager.controllerType( 35 | CREditorControl, 36 | editor, 37 | ) 38 | if (!controller) { 39 | return undefined 40 | } 41 | if ( 42 | type === TEventRangeType.keyboard && 43 | atom.config.get('ide-haskell.onCursorMove', { 44 | scope: editor.getRootScopeDescriptor(), 45 | }) !== 'Show Tooltip' 46 | ) { 47 | return undefined 48 | } 49 | const msg = controller.getMessageAt(crange.start, type) 50 | if (msg.length > 0) { 51 | return { range: crange, text: msg } 52 | } 53 | return undefined 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/config-params/index.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable } from 'atom' 2 | import { ParamControl } from './param-control' 3 | import { ConfigParamStore, IState as IStoreState } from './param-store' 4 | 5 | import { OutputPanel } from '../output-panel' 6 | import * as UPI from 'atom-haskell-upi' 7 | 8 | type IState = IStoreState 9 | export { IState } 10 | 11 | export class ConfigParamManager { 12 | private store: ConfigParamStore 13 | constructor(private outputPanel: OutputPanel, state: IState) { 14 | this.store = new ConfigParamStore(state) 15 | } 16 | 17 | public destroy() { 18 | this.store.destroy() 19 | } 20 | 21 | public serialize() { 22 | return this.store.serialize() 23 | } 24 | 25 | public add( 26 | pluginName: string, 27 | paramName: string, 28 | spec: UPI.IParamSpec, 29 | ) { 30 | const disp = new CompositeDisposable() 31 | disp.add( 32 | this.store.addParamSpec(pluginName, paramName, spec), 33 | this.outputPanel.addPanelControl({ 34 | element: ParamControl, 35 | opts: { 36 | pluginName, 37 | name: paramName, 38 | spec, 39 | store: this.store, 40 | }, 41 | }), 42 | ) 43 | return disp 44 | } 45 | 46 | public async get(pluginName: string, name: string) { 47 | return this.store.getValue(pluginName, name) 48 | } 49 | 50 | public async set(pluginName: string, name: string, value?: T) { 51 | return this.store.setValue(pluginName, name, value) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/config-params/param-control.tsx: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, Disposable } from 'atom' 2 | import * as etch from 'etch' 3 | import * as UPI from 'atom-haskell-upi' 4 | 5 | import { ConfigParamStore } from './param-store' 6 | import { handlePromise } from '../utils' 7 | 8 | export interface IProps { 9 | pluginName: string 10 | name: string 11 | spec: UPI.IParamSpec 12 | store: ConfigParamStore 13 | } 14 | 15 | export class ParamControl implements UPI.IElementObject> { 16 | public element!: HTMLElement 17 | private disposables: CompositeDisposable 18 | private hiddenValue: boolean 19 | private value?: T 20 | private storeDisposable?: Disposable 21 | constructor(public props: IProps) { 22 | this.disposables = new CompositeDisposable() 23 | 24 | this.hiddenValue = atom.config.get('ide-haskell.hideParameterValues') 25 | 26 | this.initStore() 27 | 28 | this.initSpec() 29 | 30 | etch.initialize(this) 31 | 32 | this.disposables.add( 33 | atom.config.onDidChange( 34 | 'ide-haskell.hideParameterValues', 35 | ({ newValue }) => { 36 | this.hiddenValue = newValue 37 | handlePromise(this.update()) 38 | }, 39 | ), 40 | ) 41 | 42 | this.disposables.add( 43 | atom.tooltips.add(this.element, { title: this.tooltipTitle }), 44 | ) 45 | } 46 | 47 | public render() { 48 | const classList = [ 49 | `ide-haskell--${this.props.pluginName}`, 50 | `ide-haskell-param--${this.props.name}`, 51 | ] 52 | if (this.hiddenValue) { 53 | classList.push('hidden-value') 54 | } 55 | return ( 56 | this.setValue() }} 59 | > 60 | 61 | {this.props.spec.displayTemplate(this.value)} 62 | 63 | 64 | ) 65 | } 66 | 67 | public async update(props?: Partial>) { 68 | if (props) { 69 | const { pluginName, name, spec, store } = props 70 | if (pluginName !== undefined) { 71 | this.props.pluginName = pluginName 72 | } 73 | if (name !== undefined) { 74 | this.props.name = name 75 | } 76 | if (spec && this.props.spec !== spec) { 77 | this.props.spec = spec 78 | this.initSpec() 79 | } 80 | if (store && this.props.store !== store) { 81 | this.props.store = store 82 | this.initStore() 83 | } 84 | } 85 | return etch.update(this) 86 | } 87 | 88 | public async setValue(e?: T) { 89 | await this.props.store.setValue(this.props.pluginName, this.props.name, e) 90 | handlePromise(this.update()) 91 | } 92 | 93 | public async destroy() { 94 | await etch.destroy(this) 95 | this.disposables.dispose() 96 | } 97 | 98 | private initStore() { 99 | if (this.storeDisposable) { 100 | this.disposables.remove(this.storeDisposable) 101 | } 102 | this.storeDisposable = this.props.store.onDidUpdate( 103 | this.props.pluginName, 104 | this.props.name, 105 | ({ value }) => { 106 | this.value = value 107 | handlePromise(this.update()) 108 | }, 109 | ) 110 | this.disposables.add(this.storeDisposable) 111 | handlePromise(this.setValueInitial()) 112 | } 113 | 114 | private async setValueInitial() { 115 | this.value = await this.props.store.getValueRaw( 116 | this.props.pluginName, 117 | this.props.name, 118 | ) 119 | return this.update() 120 | } 121 | 122 | private initSpec() { 123 | if (this.props.spec.displayName === undefined) { 124 | this.props.spec.displayName = 125 | this.props.name.charAt(0).toUpperCase() + this.props.name.slice(1) 126 | } 127 | } 128 | 129 | private tooltipTitle = (): string => { 130 | const displayName = 131 | this.props.spec.displayName !== undefined 132 | ? this.props.spec.displayName 133 | : 'Undefined name' 134 | if (this.hiddenValue) { 135 | return `${displayName}: ${this.props.spec.displayTemplate(this.value)}` 136 | } else { 137 | return displayName 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/config-params/param-select-view.ts: -------------------------------------------------------------------------------- 1 | import SelectListView from 'atom-select-list' 2 | import { Panel } from 'atom' 3 | 4 | export interface ISelectListParams { 5 | items: T[] | Promise 6 | heading?: string 7 | itemTemplate?: (item: T) => string 8 | itemFilterKey?: string | ((item: T) => string) 9 | activeItem?: T 10 | } 11 | 12 | export async function selectListView({ 13 | items, 14 | heading, 15 | itemTemplate, 16 | itemFilterKey, 17 | activeItem, 18 | }: ISelectListParams): Promise { 19 | const elementForItem = (item: T) => { 20 | const li = document.createElement('li') 21 | const div = document.createElement('div') 22 | div.style.display = 'inline-block' 23 | let isActive 24 | if (itemTemplate) { 25 | div.innerHTML = itemTemplate(item) 26 | isActive = activeItem && itemTemplate(item) === itemTemplate(activeItem) 27 | } else { 28 | div.innerText = `${item}` 29 | isActive = activeItem && item === activeItem 30 | } 31 | if (isActive) li.classList.add('active') 32 | // hack for backwards compatibility 33 | if (div.firstElementChild && div.firstElementChild.tagName === 'LI') { 34 | div.innerHTML = div.firstElementChild.innerHTML 35 | } 36 | li.appendChild(div) 37 | return li 38 | } 39 | const filterKeyForItem = (item: T) => { 40 | if (typeof itemFilterKey === 'string') { 41 | return `${item[itemFilterKey]}` 42 | } else if (itemFilterKey) { 43 | return itemFilterKey(item) 44 | } else { 45 | return `${item}` 46 | } 47 | } 48 | const myitems = await Promise.resolve(items) 49 | let panel: Panel> | undefined 50 | const currentFocus = document.activeElement as HTMLElement | undefined | null 51 | try { 52 | return await new Promise((resolve) => { 53 | const select = new SelectListView({ 54 | items: myitems, 55 | infoMessage: heading, 56 | itemsClassList: ['ide-haskell', 'mark-active'], 57 | elementForItem, 58 | filterKeyForItem, 59 | didCancelSelection: () => { 60 | resolve(undefined) 61 | }, 62 | didConfirmSelection: (item: T) => { 63 | resolve(item) 64 | }, 65 | }) 66 | select.element.classList.add('ide-haskell') 67 | panel = atom.workspace.addModalPanel({ 68 | item: select, 69 | visible: true, 70 | }) 71 | select.focus() 72 | }) 73 | } finally { 74 | panel?.destroy() 75 | currentFocus?.focus() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/config-params/param-store.ts: -------------------------------------------------------------------------------- 1 | import { selectListView } from './param-select-view' 2 | import { Emitter, CompositeDisposable, Disposable } from 'atom' 3 | import * as UPI from 'atom-haskell-upi' 4 | 5 | interface IParamData { 6 | spec: UPI.IParamSpec 7 | value?: T 8 | } 9 | 10 | export interface IState { 11 | [pluginNameParamName: string]: Object 12 | } 13 | 14 | interface TUpdatedCallbackArg { 15 | pluginName: string 16 | paramName: string 17 | value: T | undefined 18 | } 19 | export type TUpdatedCallback = (arg: TUpdatedCallbackArg) => void 20 | 21 | export class ConfigParamStore { 22 | private disposables: CompositeDisposable 23 | private emitter: Emitter< 24 | {}, 25 | { 26 | 'did-update': { pluginName: string; paramName: string; value: any } 27 | } 28 | > 29 | private saved: IState 30 | private plugins: Map>> 31 | constructor(state: IState = {}) { 32 | this.disposables = new CompositeDisposable() 33 | this.emitter = new Emitter() 34 | this.disposables.add(this.emitter) 35 | this.saved = state 36 | this.plugins = new Map() 37 | } 38 | 39 | public serialize() { 40 | return this.saved 41 | } 42 | 43 | public destroy() { 44 | this.disposables.dispose() 45 | } 46 | 47 | public onDidUpdate( 48 | pluginName: string, 49 | paramName: string, 50 | callback: TUpdatedCallback, 51 | ) { 52 | return this.emitter.on('did-update', (val) => { 53 | if (val.pluginName === pluginName && val.paramName === paramName) { 54 | callback(val) 55 | } 56 | }) 57 | } 58 | 59 | public addParamSpec( 60 | pluginName: string, 61 | paramName: string, 62 | spec: UPI.IParamSpec, 63 | ) { 64 | let pluginConfig = this.plugins.get(pluginName) 65 | if (!pluginConfig) { 66 | pluginConfig = new Map() 67 | this.plugins.set(pluginName, pluginConfig) 68 | } 69 | if (pluginConfig.has(paramName)) { 70 | throw new Error(`Parameter ${pluginName}.${paramName} already defined!`) 71 | } 72 | let value: T | undefined = this.saved[`${pluginName}.${paramName}`] as T 73 | if (value === undefined) { 74 | value = spec.default 75 | } 76 | pluginConfig.set(paramName, { spec, value }) 77 | this.emitter.emit('did-update', { pluginName, paramName, value }) 78 | return new Disposable(() => { 79 | if (pluginConfig) { 80 | pluginConfig.delete(paramName) 81 | if (pluginConfig.size === 0) { 82 | this.plugins.delete(pluginName) 83 | } 84 | } 85 | }) 86 | } 87 | 88 | public async setValue( 89 | pluginName: string, 90 | paramName: string, 91 | value?: T, 92 | ): Promise { 93 | const paramConfig = await this.getParamConfig( 94 | pluginName, 95 | paramName, 96 | 'set', 97 | ) 98 | if (paramConfig === undefined) return undefined 99 | if (value === undefined) { 100 | value = await this.showSelect(paramConfig) 101 | } 102 | if (value !== undefined) { 103 | paramConfig.value = value 104 | this.saved[`${pluginName}.${paramName}`] = value 105 | this.emitter.emit('did-update', { pluginName, paramName, value }) 106 | } 107 | return value 108 | } 109 | 110 | public async getValue( 111 | pluginName: string, 112 | paramName: string, 113 | ): Promise { 114 | const paramConfig = await this.getParamConfig( 115 | pluginName, 116 | paramName, 117 | 'get', 118 | ) 119 | if (paramConfig === undefined) return undefined 120 | if (paramConfig.value === undefined) { 121 | await this.setValue(pluginName, paramName) 122 | } 123 | return paramConfig.value 124 | } 125 | 126 | public async getValueRaw( 127 | pluginName: string, 128 | paramName: string, 129 | ): Promise { 130 | const paramConfig = await this.getParamConfig( 131 | pluginName, 132 | paramName, 133 | 'get raw', 134 | ) 135 | if (paramConfig === undefined) return undefined 136 | return paramConfig.value 137 | } 138 | 139 | private async getParamConfig( 140 | pluginName: string, 141 | paramName: string, 142 | reason: string, 143 | ): Promise | undefined> { 144 | if (!atom.packages.isPackageLoaded(pluginName)) { 145 | console.error( 146 | new Error( 147 | `No ${pluginName} package while trying to ${reason} ${pluginName}.${paramName}`, 148 | ), 149 | ) 150 | return undefined 151 | } 152 | if (!atom.packages.isPackageActive(pluginName)) { 153 | await atom.packages.activatePackage(pluginName) 154 | } 155 | const pluginConfig = this.plugins.get(pluginName) 156 | if (!pluginConfig) { 157 | throw new Error( 158 | `${pluginName} is not defined while trying to ${reason} ${pluginName}.${paramName}`, 159 | ) 160 | } 161 | const paramConfig = pluginConfig.get(paramName) 162 | if (!paramConfig) { 163 | throw new Error( 164 | `${paramName} is not defined while trying to ${reason} ${pluginName}.${paramName}`, 165 | ) 166 | } 167 | return paramConfig 168 | } 169 | 170 | private async showSelect(param: IParamData): Promise { 171 | const spec = param.spec 172 | return selectListView({ 173 | items: typeof spec.items === 'function' ? spec.items() : spec.items, 174 | heading: spec.description, 175 | itemTemplate: spec.itemTemplate.bind(spec), 176 | itemFilterKey: spec.itemFilterKey, 177 | activeItem: param.value, 178 | }) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/editor-control/editor-overlay-manager.ts: -------------------------------------------------------------------------------- 1 | import { EventTable } from './event-table' 2 | import * as AtomTypes from 'atom' 3 | import * as UPI from 'atom-haskell-upi' 4 | import TEventRangeType = UPI.TEventRangeType 5 | 6 | type Range = AtomTypes.Range 7 | type TextEditor = AtomTypes.TextEditor 8 | type DisplayMarker = AtomTypes.DisplayMarker 9 | 10 | export interface IMarkerProperties extends AtomTypes.FindDisplayMarkerOptions { 11 | persistent: boolean 12 | } 13 | 14 | export class EditorOverlayManager { 15 | private markers: EventTable 16 | private editorElement: HTMLElement 17 | constructor(private editor: TextEditor) { 18 | this.markers = new EventTable(editor, [ 19 | [{ type: TEventRangeType.keyboard }, { type: TEventRangeType.context }], 20 | [{ type: TEventRangeType.mouse }, { type: TEventRangeType.selection }], 21 | ]) 22 | this.editorElement = atom.views.getView(this.editor) 23 | } 24 | 25 | public dispose() { 26 | this.markers.destroy() 27 | this.editorElement.classList.remove('ide-haskell--has-tooltips') 28 | } 29 | 30 | public show( 31 | range: Range, 32 | type: TEventRangeType, 33 | source: string, 34 | detail: IMarkerProperties, 35 | view: object, 36 | ) { 37 | this.hide(type, source) 38 | const highlightMarker = this.markers 39 | .get(type, source) 40 | .markBufferRange(range) 41 | highlightMarker.setProperties(detail) 42 | this.decorate(highlightMarker, view) 43 | this.editorElement.classList.add('ide-haskell--has-tooltips') 44 | } 45 | 46 | public hide( 47 | type?: TEventRangeType, 48 | source?: string, 49 | template?: IMarkerProperties, 50 | ) { 51 | if (type === undefined) { 52 | this.markers.clear() 53 | return 54 | } 55 | if (!template) { 56 | this.markers.get(type, source).clear() 57 | } else { 58 | this.markers 59 | .get(type, source) 60 | .findMarkers(template) 61 | .forEach((m: DisplayMarker) => m.destroy()) 62 | } 63 | if (!this.has()) { 64 | this.editorElement.classList.remove('ide-haskell--has-tooltips') 65 | } 66 | } 67 | 68 | public has( 69 | type?: TEventRangeType, 70 | source?: string, 71 | template?: IMarkerProperties, 72 | ) { 73 | if (type === undefined) { 74 | return this.markers.getMarkerCount() > 0 75 | } 76 | if (!template) { 77 | return this.markers.get(type, source).getMarkerCount() > 0 78 | } else { 79 | return this.markers.get(type, source).findMarkers(template).length > 0 80 | } 81 | } 82 | 83 | private decorate(marker: DisplayMarker, tooltipView: object) { 84 | this.editor.decorateMarker(marker, { 85 | type: 'overlay', 86 | position: 'tail', 87 | item: tooltipView, 88 | }) 89 | this.editor.decorateMarker(marker, { 90 | type: 'highlight', 91 | class: 'ide-haskell-type', 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/editor-control/event-table.ts: -------------------------------------------------------------------------------- 1 | import { TextEditor, DisplayMarkerLayer } from 'atom' 2 | import { eventRangeTypeVals } from '../utils' 3 | import * as UPI from 'atom-haskell-upi' 4 | import TEventRangeType = UPI.TEventRangeType 5 | 6 | export type IMarkerGroup = Array<{ type: TEventRangeType; source?: string }> 7 | 8 | export type TTableCell = Map 9 | 10 | export type TTable = { [K in TEventRangeType]: TTableCell } 11 | 12 | export class EventTable { 13 | private table: TTable 14 | private layers: Set 15 | constructor(private editor: TextEditor, groups: IMarkerGroup[]) { 16 | // tslint:disable-next-line:no-null-keyword 17 | this.table = Object.create(null) 18 | for (const i of eventRangeTypeVals) { 19 | this.table[i] = new Map() 20 | } 21 | this.layers = new Set() 22 | for (const i of groups) { 23 | const layer = this.editor.addMarkerLayer() 24 | this.layers.add(layer) 25 | for (const { type, source } of i) { 26 | ;(this.table[type] as TTableCell).set(source, layer) 27 | } 28 | } 29 | } 30 | 31 | public destroy() { 32 | for (const i of this.layers.values()) { 33 | i.destroy() 34 | } 35 | for (const i of eventRangeTypeVals) { 36 | this.table[i].clear() 37 | } 38 | } 39 | 40 | public get(type: TEventRangeType, source?: string) { 41 | const tbl = this.table[type] as Map 42 | let res = tbl.get(source) 43 | if (!res) { 44 | res = tbl.get(undefined) 45 | } 46 | if (!res) { 47 | throw new Error(`Failed to classify ${type}:${source}`) 48 | } 49 | return res 50 | } 51 | 52 | public clear() { 53 | for (const i of this.layers.values()) { 54 | i.clear() 55 | } 56 | } 57 | 58 | public getMarkerCount() { 59 | let count = 0 60 | for (const i of this.layers.values()) { 61 | count += i.getMarkerCount() 62 | } 63 | return count 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/editor-control/index.ts: -------------------------------------------------------------------------------- 1 | import { Range, TextEditor, Point, CompositeDisposable, Disposable } from 'atom' 2 | import { bufferPositionFromMouseEvent, listen, handlePromise } from '../utils' 3 | import { EditorOverlayManager } from './editor-overlay-manager' 4 | import { TooltipManager as GlobalTooltipManager } from '../tooltip-manager' 5 | import { PluginManager, IEditorController } from '../plugin-manager' 6 | import * as UPI from 'atom-haskell-upi' 7 | import TEventRangeType = UPI.TEventRangeType 8 | 9 | export type TEventRangeResult = 10 | | { crange: Range; pos: Point; eventType: TEventRangeType } 11 | | undefined 12 | 13 | export class EditorControl implements IEditorController { 14 | public readonly tooltips: EditorOverlayManager 15 | private readonly disposables: CompositeDisposable 16 | private lastMouseBufferPt?: Point 17 | private exprTypeTimeout?: number 18 | private selTimeout?: number 19 | private readonly editorElement: HTMLElement & { 20 | onDidChangeScrollTop(a: () => void): Disposable 21 | onDidChangeScrollLeft(a: () => void): Disposable 22 | pixelRectForScreenRange( 23 | r: Range, 24 | ): { 25 | left: number 26 | top: number 27 | width: number 28 | height: number 29 | } 30 | } 31 | private readonly tooltipManager: GlobalTooltipManager 32 | constructor( 33 | private readonly editor: TextEditor, 34 | pluginManager: PluginManager, 35 | ) { 36 | this.disposables = new CompositeDisposable() 37 | this.tooltips = new EditorOverlayManager(this.editor) 38 | this.disposables.add(this.tooltips) 39 | this.tooltipManager = pluginManager.tooltipManager 40 | 41 | this.editorElement = atom.views.getView(this.editor) as any 42 | 43 | const buffer = this.editor.getBuffer() 44 | 45 | this.disposables.add( 46 | // buffer events for automatic check 47 | buffer.onWillSave(async () => { 48 | await pluginManager.willSaveBuffer(buffer) 49 | }), 50 | buffer.onDidSave(() => pluginManager.didSaveBuffer(buffer)), 51 | this.editor.onDidStopChanging(() => 52 | pluginManager.didStopChanging(buffer), 53 | ), 54 | // tooltip tracking (mouse and selection) 55 | this.editorElement.onDidChangeScrollLeft(() => 56 | this.tooltips.hide(TEventRangeType.mouse), 57 | ), 58 | this.editorElement.onDidChangeScrollTop(() => 59 | this.tooltips.hide(TEventRangeType.mouse), 60 | ), 61 | listen( 62 | this.editorElement, 63 | 'mousemove', 64 | '.scroll-view', 65 | this.trackMouseBufferPosition, 66 | ), 67 | listen( 68 | this.editorElement, 69 | 'mouseout', 70 | '.scroll-view', 71 | this.stopTrackingMouseBufferPosition, 72 | ), 73 | this.editor.onDidChangeSelectionRange(this.trackSelection), 74 | ) 75 | } 76 | 77 | public static supportsGrammar(grammar: string): boolean { 78 | return [ 79 | // 'source.c2hs', 80 | // 'source.cabal', 81 | // 'source.hsc2hs', 82 | 'source.haskell', 83 | 'text.tex.latex.haskell', 84 | // 'source.hsig', 85 | ].includes(grammar) 86 | } 87 | 88 | public destroy() { 89 | if (this.exprTypeTimeout !== undefined) { 90 | clearTimeout(this.exprTypeTimeout) 91 | } 92 | if (this.selTimeout !== undefined) { 93 | clearTimeout(this.selTimeout) 94 | } 95 | this.disposables.dispose() 96 | this.lastMouseBufferPt = undefined 97 | } 98 | 99 | public getEventRange(eventType: TEventRangeType): TEventRangeResult { 100 | let crange: Range 101 | let pos: Point 102 | switch (eventType) { 103 | case 'mouse': 104 | case 'context': 105 | if (!this.lastMouseBufferPt) return undefined 106 | pos = this.lastMouseBufferPt 107 | const selRanges = this.editor 108 | .getSelections() 109 | .map((sel) => sel.getBufferRange()) 110 | .filter((sel) => sel.containsPoint(pos)) 111 | crange = selRanges.length > 0 ? selRanges[0] : new Range(pos, pos) 112 | break 113 | case 'keyboard': 114 | case 'selection': 115 | crange = this.editor.getLastSelection().getBufferRange() 116 | pos = crange.start 117 | break 118 | default: 119 | throw new TypeError('Switch assertion failed') 120 | } 121 | 122 | return { crange, pos, eventType } 123 | } 124 | 125 | private shouldShowTooltip(pos: Point, type: TEventRangeType) { 126 | if ( 127 | pos.row < 0 || 128 | pos.row >= this.editor.getLineCount() || 129 | pos.isEqual(this.editor.getBuffer().rangeForRow(pos.row, false).end) 130 | ) { 131 | this.tooltips.hide(type) 132 | } else { 133 | handlePromise(this.tooltipManager.showTooltip(this.editor, type)) 134 | } 135 | } 136 | 137 | private trackMouseBufferPosition = (e: MouseEvent) => { 138 | const bufferPt = bufferPositionFromMouseEvent(this.editor, e) 139 | if (!bufferPt) { 140 | return 141 | } 142 | 143 | if (this.lastMouseBufferPt && this.lastMouseBufferPt.isEqual(bufferPt)) { 144 | return 145 | } 146 | this.lastMouseBufferPt = bufferPt 147 | 148 | if (this.exprTypeTimeout !== undefined) { 149 | clearTimeout(this.exprTypeTimeout) 150 | } 151 | this.exprTypeTimeout = window.setTimeout( 152 | () => { 153 | this.shouldShowTooltip(bufferPt, TEventRangeType.mouse) 154 | }, 155 | atom.config.get('ide-haskell.expressionTypeInterval', { 156 | scope: this.editor.getRootScopeDescriptor(), 157 | }), 158 | ) 159 | } 160 | 161 | private stopTrackingMouseBufferPosition = () => { 162 | if (this.exprTypeTimeout !== undefined) { 163 | return clearTimeout(this.exprTypeTimeout) 164 | } 165 | } 166 | 167 | private trackSelection = ({ newBufferRange }: { newBufferRange: Range }) => { 168 | this.handleCursorUnderTooltip(newBufferRange) 169 | 170 | if (this.selTimeout !== undefined) { 171 | clearTimeout(this.selTimeout) 172 | } 173 | if (newBufferRange.isEmpty()) { 174 | this.tooltips.hide(TEventRangeType.selection) 175 | if (this.exprTypeTimeout !== undefined) { 176 | clearTimeout(this.exprTypeTimeout) 177 | } 178 | 179 | handlePromise( 180 | this.tooltipManager.showTooltip(this.editor, TEventRangeType.keyboard), 181 | ) 182 | if ( 183 | atom.config.get('ide-haskell.onCursorMove', { 184 | scope: this.editor.getRootScopeDescriptor(), 185 | }) === 'Hide Tooltip' 186 | ) { 187 | this.tooltips.hide(TEventRangeType.mouse, undefined, { 188 | persistent: false, 189 | }) 190 | this.tooltips.hide(TEventRangeType.context, undefined, { 191 | persistent: false, 192 | }) 193 | } 194 | } else { 195 | this.selTimeout = window.setTimeout( 196 | () => 197 | this.shouldShowTooltip( 198 | newBufferRange.start, 199 | TEventRangeType.selection, 200 | ), 201 | atom.config.get('ide-haskell.expressionTypeInterval', { 202 | scope: this.editor.getRootScopeDescriptor(), 203 | }), 204 | ) 205 | } 206 | } 207 | 208 | private handleCursorUnderTooltip(currentRange: Range) { 209 | const tooltipElement = document.querySelector('ide-haskell-tooltip') 210 | if (!tooltipElement) { 211 | return 212 | } 213 | const slcl = this.editorElement.pixelRectForScreenRange( 214 | this.editor.screenRangeForBufferRange(currentRange), 215 | ) 216 | const sv = this.editorElement.querySelector('.scroll-view') 217 | if (!sv) { 218 | return 219 | } 220 | const eecl = sv.getBoundingClientRect() 221 | const ttcl = tooltipElement.getBoundingClientRect() 222 | const div = tooltipElement.querySelector('div') 223 | if (!div) { 224 | return 225 | } 226 | const ttcld = div.getBoundingClientRect() 227 | const ttbox = { 228 | left: ttcl.left - eecl.left, 229 | top: ttcld.top - eecl.top, 230 | width: ttcl.width, 231 | height: ttcld.height, 232 | } 233 | const xmax = Math.round(Math.max(ttbox.left, slcl.left)) 234 | const xmin = Math.round( 235 | Math.min(ttbox.left + ttbox.width, slcl.left + slcl.width), 236 | ) 237 | const ymax = Math.round(Math.max(ttbox.top, slcl.top)) 238 | const ymin = Math.round( 239 | Math.min(ttbox.top + ttbox.height, slcl.top + slcl.height), 240 | ) 241 | const tt = document.querySelector( 242 | 'ide-haskell-tooltip', 243 | ) as HTMLElement | null 244 | if (tt) { 245 | if (ymax <= ymin && xmax <= xmin) { 246 | tt.classList.add('ide-haskell-tooltip-subdued') 247 | } else { 248 | tt.classList.remove('ide-haskell-tooltip-subdued') 249 | } 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/editor-mark-control/index.ts: -------------------------------------------------------------------------------- 1 | import { TextEditor } from 'atom' 2 | 3 | import { IEditorController } from '../plugin-manager' 4 | 5 | export class EditorMarkControl implements IEditorController { 6 | private editorElement: HTMLElement 7 | constructor(private editor: TextEditor) { 8 | this.editorElement = atom.views.getView(this.editor) as any 9 | this.editorElement.classList.add('ide-haskell') 10 | } 11 | 12 | public static supportsGrammar(grammar: string): boolean { 13 | return [ 14 | 'source.c2hs', 15 | 'source.cabal', 16 | 'source.hsc2hs', 17 | 'source.haskell', 18 | 'text.tex.latex.haskell', 19 | 'source.hsig', 20 | ].includes(grammar) 21 | } 22 | 23 | public destroy() { 24 | this.editorElement.classList.remove('ide-haskell') 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ide-haskell.ts: -------------------------------------------------------------------------------- 1 | import { PluginManager, IState } from './plugin-manager' 2 | import { prettifyFile } from './prettify' 3 | import { MAIN_MENU_LABEL, handlePromise } from './utils' 4 | import * as UPI3 from './upi-3' 5 | import * as OutputPanel from './output-panel' 6 | import * as AtomTypes from 'atom' 7 | import * as UPI from 'atom-haskell-upi' 8 | import * as Linter from 'atom/linter' 9 | import * as StatusBar from 'atom/status-bar' 10 | import CompositeDisposable = AtomTypes.CompositeDisposable 11 | import Disposable = AtomTypes.Disposable 12 | 13 | let upiProvided = false 14 | let disposables: CompositeDisposable | undefined 15 | let pluginManager: PluginManager | undefined 16 | let outputPanel: OutputPanel.OutputPanel | undefined 17 | let menu: CompositeDisposable | undefined 18 | 19 | function cleanConfig() { 20 | /*noop*/ 21 | } 22 | 23 | export function activate(state: IState) { 24 | cleanConfig() 25 | 26 | atom.views.getView(atom.workspace).classList.add('ide-haskell') 27 | 28 | require('etch').setScheduler(atom.views) 29 | 30 | upiProvided = false 31 | 32 | if (atom.config.get('ide-haskell.startupMessageIdeBackend')) { 33 | setTimeout(() => { 34 | if (!upiProvided) { 35 | atom.notifications.addWarning( 36 | `Ide-Haskell needs backends that provide most of functionality. 37 | Please refer to README for details`, 38 | { dismissable: true }, 39 | ) 40 | } 41 | }, 5000) 42 | } 43 | 44 | disposables = new CompositeDisposable() 45 | 46 | pluginManager = new PluginManager(state, deserializeOutputPanel()) 47 | 48 | // global commands 49 | disposables.add( 50 | atom.commands.add('atom-workspace', { 51 | 'ide-haskell:toggle-output': () => { 52 | if (pluginManager) pluginManager.togglePanel() 53 | }, 54 | 'ide-haskell:next-error': () => { 55 | if (pluginManager) pluginManager.nextError() 56 | }, 57 | 'ide-haskell:prev-error': () => { 58 | if (pluginManager) pluginManager.prevError() 59 | }, 60 | }), 61 | atom.commands.add('atom-text-editor.ide-haskell', { 62 | 'ide-haskell:prettify-file': ({ currentTarget }) => { 63 | handlePromise(prettifyFile(currentTarget.getModel())) 64 | }, 65 | }), 66 | atom.commands.add('atom-text-editor.ide-haskell--has-tooltips', { 67 | 'ide-haskell:close-tooltip': ({ currentTarget, abortKeyBinding }) => { 68 | const controller = 69 | pluginManager && pluginManager.controller(currentTarget.getModel()) 70 | if (controller && controller.tooltips.has()) { 71 | controller.tooltips.hide() 72 | } else { 73 | abortKeyBinding() 74 | } 75 | }, 76 | }), 77 | ) 78 | 79 | menu = new CompositeDisposable() 80 | menu.add( 81 | atom.menu.add([ 82 | { 83 | label: MAIN_MENU_LABEL, 84 | submenu: [ 85 | { label: 'Prettify', command: 'ide-haskell:prettify-file' }, 86 | { label: 'Toggle Panel', command: 'ide-haskell:toggle-output' }, 87 | ], 88 | }, 89 | ]), 90 | ) 91 | } 92 | 93 | export function deactivate() { 94 | if (pluginManager) pluginManager.deactivate() 95 | 96 | // clear commands 97 | if (disposables) disposables.dispose() 98 | 99 | if (menu) menu.dispose() 100 | atom.menu.update() 101 | 102 | disposables = undefined 103 | pluginManager = undefined 104 | menu = undefined 105 | outputPanel = undefined 106 | } 107 | 108 | export function serialize() { 109 | if (pluginManager) { 110 | return pluginManager.serialize() 111 | } 112 | return undefined 113 | } 114 | 115 | export function deserializeOutputPanel(state?: OutputPanel.IState) { 116 | if (!outputPanel) outputPanel = new OutputPanel.OutputPanel(state) 117 | return outputPanel 118 | } 119 | 120 | function provideUpi3(features: UPI3.FeatureSet = {}) { 121 | return function(): UPI.IUPIRegistration { 122 | upiProvided = true 123 | return (options: UPI.IRegistrationOptions) => { 124 | if (!pluginManager) { 125 | throw new Error( 126 | 'IDE-Haskell failed to provide UPI instance: pluginManager is undefined', 127 | ) 128 | } 129 | return UPI3.instance(pluginManager, options, features) 130 | } 131 | } 132 | } 133 | 134 | // tslint:disable-next-line: variable-name 135 | export const provideUpi3_0 = provideUpi3() 136 | // tslint:disable-next-line: variable-name 137 | export const provideUpi3_1 = provideUpi3({ eventsReturnResults: true }) 138 | // tslint:disable-next-line: variable-name 139 | export const provideUpi3_2 = provideUpi3({ 140 | eventsReturnResults: true, 141 | supportsCommands: true, 142 | }) 143 | // tslint:disable-next-line: variable-name 144 | export const provideUpi3_3 = provideUpi3({ 145 | eventsReturnResults: true, 146 | supportsCommands: true, 147 | supportsActions: true, 148 | }) 149 | 150 | function consumeUpi3(features: UPI3.FeatureSet = {}) { 151 | return function( 152 | registration: UPI.IRegistrationOptions, 153 | ): Disposable | undefined { 154 | upiProvided = true 155 | if (pluginManager) { 156 | return UPI3.consume(pluginManager, registration, features) 157 | } 158 | return undefined 159 | } 160 | } 161 | 162 | // tslint:disable-next-line: variable-name 163 | export const consumeUpi3_0 = consumeUpi3({}) 164 | // tslint:disable-next-line: variable-name 165 | export const consumeUpi3_1 = consumeUpi3({ eventsReturnResults: true }) 166 | // tslint:disable-next-line: variable-name 167 | export const consumeUpi3_2 = consumeUpi3({ 168 | eventsReturnResults: true, 169 | supportsCommands: true, 170 | }) 171 | 172 | export function consumeLinter( 173 | register: (opts: {}) => Linter.IndieDelegate, 174 | ): Disposable | undefined { 175 | if (!(disposables && pluginManager)) { 176 | return undefined 177 | } 178 | const linter = register({ name: 'IDE-Haskell' }) 179 | disposables.add(linter) 180 | pluginManager.setLinter(linter) 181 | return linter 182 | } 183 | 184 | export function consumeStatusBar( 185 | statusBar: StatusBar.StatusBar, 186 | ): Disposable | undefined { 187 | if (!pluginManager) { 188 | return undefined 189 | } 190 | pluginManager.setStatusBar(statusBar) 191 | return new Disposable(() => { 192 | if (pluginManager) { 193 | pluginManager.removeStatusBar() 194 | } 195 | }) 196 | } 197 | -------------------------------------------------------------------------------- /src/linter-support/index.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, Range } from 'atom' 2 | import { ResultsDB } from '../results-db' 3 | import * as Linter from 'atom/linter' 4 | 5 | export class LinterSupport { 6 | private disposables: CompositeDisposable 7 | constructor( 8 | private linter: Linter.IndieDelegate, 9 | private resultDb: ResultsDB, 10 | ) { 11 | this.disposables = new CompositeDisposable() 12 | 13 | this.disposables.add(resultDb.onDidUpdate(this.update)) 14 | } 15 | 16 | public destroy() { 17 | this.disposables.dispose() 18 | this.linter.dispose() 19 | } 20 | 21 | public update = () => { 22 | this.linter.clearMessages() 23 | this.linter.setAllMessages(Array.from(this.messages())) 24 | } 25 | 26 | private *messages(): IterableIterator { 27 | for (const result of this.resultDb.results()) { 28 | if (result.uri !== undefined && result.position) { 29 | let severity: 'error' | 'warning' | 'info' 30 | switch (result.severity) { 31 | case 'error': 32 | case 'warning': 33 | severity = result.severity 34 | break 35 | default: 36 | severity = 'info' 37 | break 38 | } 39 | yield { 40 | severity, 41 | excerpt: result.message.toPlain(), 42 | location: { 43 | file: result.uri, 44 | position: new Range( 45 | result.position, 46 | result.position.translate([0, 1]), 47 | ), 48 | }, 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/output-panel/index.tsx: -------------------------------------------------------------------------------- 1 | import * as etch from 'etch' 2 | import { Disposable, CompositeDisposable } from 'atom' 3 | import { IBtnDesc, OutputPanelButtons } from './views/output-panel-buttons' 4 | import { OutputPanelCheckbox } from './views/output-panel-checkbox' 5 | import { OutputPanelItems } from './views/output-panel-items' 6 | import { ResultsDB, ResultItem } from '../results-db' 7 | import { isDock, isSimpleControlDef, handlePromise } from '../utils' 8 | import * as UPI from 'atom-haskell-upi' 9 | import { BackendStatusController } from '../backend-status' 10 | 11 | export interface IState { 12 | fileFilter: boolean 13 | activeTab: string 14 | } 15 | 16 | export class OutputPanel { 17 | private static readonly defaultTabs: ReadonlyArray = [ 18 | 'error', 19 | 'warning', 20 | 'lint', 21 | ] 22 | private readonly refs!: { 23 | items?: OutputPanelItems 24 | } 25 | private readonly elements: Set = new Set() 26 | private readonly disposables: CompositeDisposable = new CompositeDisposable() 27 | private readonly tabs: Map = new Map() 28 | private readonly tabUsers: Map = new Map() 29 | private itemFilter?: (item: ResultItem) => boolean 30 | private currentResult: number = 0 31 | private results?: ResultsDB 32 | private buttonsClass!: 'buttons-top' | 'buttons-left' 33 | private bsc?: BackendStatusController 34 | constructor( 35 | private state: IState = { fileFilter: false, activeTab: 'error' }, 36 | ) { 37 | this.setButtonsClass(atom.config.get('ide-haskell.buttonsPosition')) 38 | etch.initialize(this) 39 | atom.config.onDidChange('ide-haskell.buttonsPosition', ({ newValue }) => { 40 | this.setButtonsClass(newValue) 41 | handlePromise(this.update()) 42 | }) 43 | 44 | if (atom.config.get('ide-haskell.messageDisplayFrontend') === 'builtin') { 45 | for (const name of OutputPanel.defaultTabs) { 46 | this.tabs.set(name, { 47 | name, 48 | count: 0, 49 | onClick: () => this.activateTab(name), 50 | uriFilter: true, 51 | autoScroll: false, 52 | }) 53 | } 54 | } 55 | handlePromise(this.update()) 56 | 57 | this.disposables.add( 58 | atom.workspace.onDidChangeActivePaneItem(() => { 59 | if (this.state.fileFilter) handlePromise(this.updateItems()) 60 | }), 61 | ) 62 | setImmediate(async () => { 63 | await this.show() 64 | if (atom.config.get('ide-haskell.autoHideOutput')) { 65 | this.hide() 66 | } 67 | }) 68 | } 69 | 70 | public connectBsc(bsc: BackendStatusController) { 71 | if (this.bsc) throw new Error('BackendStatusController already connected!') 72 | this.bsc = bsc 73 | this.disposables.add( 74 | this.bsc.onDidUpdate(() => handlePromise(this.update())), 75 | ) 76 | 77 | handlePromise(this.update()) 78 | } 79 | 80 | public connectResults(results: ResultsDB) { 81 | if (this.results) throw new Error('Results already connected!') 82 | this.results = results 83 | 84 | let lastUpdateTime = Date.now() 85 | let collectedSeverities = new Set() 86 | const didUpdate = (severities: UPI.TSeverity[]) => { 87 | this.currentResult = 0 88 | 89 | handlePromise(this.updateItems()) 90 | const newUpdateTime = Date.now() 91 | if ( 92 | newUpdateTime - lastUpdateTime < 93 | atom.config.get('ide-haskell.switchTabOnCheckInterval') 94 | ) { 95 | for (const s of severities) { 96 | collectedSeverities.add(s) 97 | } 98 | } else { 99 | collectedSeverities = new Set(severities) 100 | } 101 | if ( 102 | atom.config.get('ide-haskell.autoHideOutput') && 103 | (!this.results || this.results.isEmpty(severities)) 104 | ) { 105 | this.hide() 106 | } else if (atom.config.get('ide-haskell.switchTabOnCheck')) { 107 | this.activateFirstNonEmptyTab(collectedSeverities) 108 | } 109 | lastUpdateTime = newUpdateTime 110 | } 111 | 112 | this.disposables.add(this.results.onDidUpdate(didUpdate)) 113 | 114 | handlePromise(this.update()) 115 | } 116 | 117 | public render() { 118 | if (!this.results) { 119 | return 120 | } 121 | // tslint:disable: strict-boolean-expressions no-null-keyword 122 | return ( 123 | 124 | 125 | {this.bsc?.renderStatusIcon() || null} 126 | 130 | 137 | {Array.from(this.elements.values())} 138 | {this.bsc?.renderProgressBar() || null} 139 | 140 | 145 | 146 | ) 147 | // tslint:enable: strict-boolean-expressions no-null-keyword 148 | } 149 | 150 | public async update() { 151 | return etch.update(this) 152 | } 153 | 154 | public destroy() { 155 | this.hide() 156 | } 157 | 158 | public async reallyDestroy() { 159 | await etch.destroy(this) 160 | this.disposables.dispose() 161 | } 162 | 163 | public async toggle() { 164 | const pane = atom.workspace.paneContainerForItem(this) 165 | if (!pane || (isDock(pane) && !pane.isVisible())) { 166 | return this.show() 167 | } else { 168 | return this.hide() 169 | } 170 | } 171 | 172 | public async show() { 173 | await atom.workspace.open(this, { 174 | searchAllPanes: true, 175 | activatePane: false, 176 | }) 177 | const pane = atom.workspace.paneContainerForItem(this) 178 | if (pane && isDock(pane)) { 179 | pane.show() 180 | } 181 | } 182 | 183 | public hide() { 184 | const pane = atom.workspace.paneContainerForItem(this) 185 | if (pane && isDock(pane)) { 186 | atom.workspace.hide(this) 187 | } 188 | } 189 | 190 | public getTitle() { 191 | return 'IDE-Haskell' 192 | } 193 | 194 | public getURI() { 195 | return `ide-haskell://output-panel/` 196 | } 197 | 198 | public getDefaultLocation() { 199 | return atom.config.get('ide-haskell.panelPosition') 200 | } 201 | 202 | public addPanelControl(def: UPI.TControlDefinition) { 203 | let newElement: JSX.Element 204 | if (isSimpleControlDef(def)) { 205 | const { events, classes, style, attrs } = def.opts 206 | const props: { [key: string]: Object } = {} 207 | if (classes) { 208 | props.class = classes.join(' ') 209 | } 210 | if (style) { 211 | props.style = style 212 | } 213 | if (attrs) { 214 | props.attributes = attrs 215 | } 216 | if (events) { 217 | props.on = events 218 | } 219 | 220 | newElement = etch.dom(def.element, props) 221 | } else { 222 | newElement = etch.dom(def.element, def.opts) 223 | } 224 | this.elements.add(newElement) 225 | 226 | handlePromise(this.update()) 227 | return new Disposable(() => { 228 | this.elements.delete(newElement) 229 | handlePromise(this.update()) 230 | }) 231 | } 232 | 233 | public async updateItems() { 234 | const activeTab = this.getActiveTab() 235 | let currentUri: string | undefined 236 | if (this.state.fileFilter) { 237 | const ed = atom.workspace.getActiveTextEditor() 238 | currentUri = ed ? ed.getPath() : undefined 239 | } 240 | let scroll: boolean = false 241 | if (activeTab) { 242 | const ato = this.tabs.get(activeTab) 243 | if (currentUri !== undefined && ato && ato.uriFilter) { 244 | this.itemFilter = ({ uri, severity }) => 245 | severity === activeTab && uri === currentUri 246 | } else { 247 | this.itemFilter = ({ severity }) => severity === activeTab 248 | } 249 | scroll = 250 | (ato && ato.autoScroll && this.refs.items && this.refs.items.atEnd()) || 251 | false 252 | } 253 | 254 | if (this.results) { 255 | for (const [btn, ato] of this.tabs.entries()) { 256 | ato.count = this.results.filter( 257 | ({ severity }) => severity === btn, 258 | ).length 259 | } 260 | } 261 | 262 | await this.update() 263 | 264 | if (scroll && this.refs.items) await this.refs.items.scrollToEnd() 265 | } 266 | 267 | public activateTab(tab: string) { 268 | this.state.activeTab = tab 269 | handlePromise(this.updateItems()) 270 | } 271 | 272 | public activateFirstNonEmptyTab(severities: Set) { 273 | for (const tab of this.tabs.values()) { 274 | if (!severities.has(tab.name)) continue 275 | const count = tab.count 276 | if (count && count > 0) { 277 | handlePromise(this.show()) 278 | this.activateTab(tab.name) 279 | break 280 | } 281 | } 282 | } 283 | 284 | public showItem(item: ResultItem) { 285 | this.activateTab(item.severity) 286 | 287 | if (this.refs.items) handlePromise(this.refs.items.showItem(item)) 288 | } 289 | 290 | public getActiveTab() { 291 | return this.state.activeTab 292 | } 293 | 294 | public async createTab( 295 | name: string, 296 | { uriFilter = true, autoScroll = false }: UPI.ISeverityTabDefinition, 297 | ) { 298 | if (OutputPanel.defaultTabs.includes(name)) return 299 | if (this.tabs.has(name)) { 300 | // tslint:disable-next-line: no-non-null-assertion 301 | this.tabUsers.set(name, this.tabUsers.get(name)! + 1) 302 | } else { 303 | this.tabUsers.set(name, 1) 304 | this.tabs.set(name, { 305 | name, 306 | count: 0, 307 | onClick: () => this.activateTab(name), 308 | uriFilter, 309 | autoScroll, 310 | }) 311 | if (this.state.activeTab) this.activateTab(this.state.activeTab) 312 | } 313 | return this.update() 314 | } 315 | 316 | public async removeTab(name: string) { 317 | if (OutputPanel.defaultTabs.includes(name)) return 318 | if (this.tabUsers.has(name)) { 319 | // tslint:disable-next-line: no-non-null-assertion 320 | let n = this.tabUsers.get(name)! 321 | n -= 1 322 | if (n === 0) { 323 | this.tabUsers.delete(name) 324 | this.tabs.delete(name) 325 | if (this.state.activeTab === name) { 326 | this.state.activeTab = OutputPanel.defaultTabs[0] 327 | } 328 | return this.update() 329 | } else { 330 | this.tabUsers.set(name, n) 331 | } 332 | } else { 333 | throw new Error( 334 | `Ide-Haskell: Removing nonexistent output panel tab ${name}`, 335 | ) 336 | } 337 | } 338 | 339 | public serialize(): IState & { deserializer: 'ide-haskell/OutputPanel' } { 340 | return { 341 | ...this.state, 342 | deserializer: 'ide-haskell/OutputPanel', 343 | } 344 | } 345 | 346 | public showNextError() { 347 | if (!this.results) return 348 | const rs = this.results.filter(({ uri }) => uri !== undefined) 349 | if (rs.length === 0) { 350 | return 351 | } 352 | 353 | this.currentResult++ 354 | if (this.currentResult >= rs.length) { 355 | this.currentResult = 0 356 | } 357 | 358 | this.showItem(rs[this.currentResult]) 359 | } 360 | 361 | public showPrevError() { 362 | if (!this.results) return 363 | const rs = this.results.filter(({ uri }) => uri !== undefined) 364 | if (rs.length === 0) { 365 | return 366 | } 367 | 368 | this.currentResult-- 369 | if (this.currentResult < 0) { 370 | this.currentResult = rs.length - 1 371 | } 372 | 373 | this.showItem(rs[this.currentResult]) 374 | } 375 | 376 | private switchFileFilter = () => { 377 | this.state.fileFilter = !this.state.fileFilter 378 | handlePromise(this.updateItems()) 379 | } 380 | 381 | private setButtonsClass(buttonsPos: 'top' | 'left') { 382 | switch (buttonsPos) { 383 | case 'top': 384 | this.buttonsClass = 'buttons-top' 385 | break 386 | case 'left': 387 | this.buttonsClass = 'buttons-left' 388 | break 389 | } 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /src/output-panel/views/output-panel-button.tsx: -------------------------------------------------------------------------------- 1 | import * as etch from 'etch' 2 | 3 | export interface IProps extends JSX.Props { 4 | active: boolean 5 | name: string 6 | count: number 7 | onClick: () => void 8 | } 9 | 10 | export class Button implements JSX.ElementClass { 11 | constructor(public props: IProps) { 12 | etch.initialize(this) 13 | } 14 | 15 | public render() { 16 | return ( 17 | 22 | ) 23 | } 24 | 25 | public async update(props: IProps) { 26 | this.props = props 27 | return etch.update(this) 28 | } 29 | 30 | public async destroy() { 31 | await etch.destroy(this) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/output-panel/views/output-panel-buttons.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from './output-panel-button' 2 | import * as etch from 'etch' 3 | 4 | export interface IBtnDesc { 5 | name: string 6 | count: number 7 | onClick: () => void 8 | uriFilter: boolean 9 | autoScroll: boolean 10 | } 11 | 12 | export interface IProps extends JSX.Props { 13 | buttons: IBtnDesc[] 14 | activeBtn?: string 15 | } 16 | 17 | export class OutputPanelButtons implements JSX.ElementClass { 18 | constructor(public props: IProps) { 19 | etch.initialize(this) 20 | } 21 | 22 | public render() { 23 | return ( 24 | 25 | {this.renderButtons()} 26 | 27 | ) 28 | } 29 | 30 | public async update(props: IProps) { 31 | this.props = props 32 | return etch.update(this) 33 | } 34 | 35 | private renderButtons() { 36 | return this.props.buttons.map((props) => ( 37 | 22 | ))} 23 | {act.length > maxActions ? ( 24 | 27 | ) : // tslint:disable-next-line: no-null-keyword 28 | null} 29 | 30 | ) 31 | } 32 | 33 | function click(editor: TextEditor) { 34 | return function() { 35 | atom.commands.dispatch( 36 | atom.views.getView(editor), 37 | 'ide-haskell:show-actions', 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/tooltip-manager/tooltip-view.tsx: -------------------------------------------------------------------------------- 1 | // tslint:disable: no-null-keyword 2 | import * as etch from 'etch' 3 | import { MessageObject, handlePromise } from '../utils' 4 | 5 | export class TooltipMessage { 6 | private message: JSX.Element[] 7 | private actions?: JSX.Element 8 | private element!: HTMLElement 9 | constructor( 10 | private source: string, 11 | message: MessageObject | MessageObject[], 12 | actions?: Promise, 13 | ) { 14 | if (Array.isArray(message)) { 15 | this.message = message.map((m) =>
) 16 | } else { 17 | this.message = [
] 18 | } 19 | if (actions) { 20 | handlePromise( 21 | actions.then(async (acts) => { 22 | this.actions = acts 23 | return this.update() 24 | }), 25 | ) 26 | } 27 | etch.initialize(this) 28 | } 29 | 30 | public render() { 31 | return ( 32 | 33 | {this.renderTooltip()} 34 | {this.actions ?? null} 35 | 36 | ) 37 | } 38 | 39 | public async update() { 40 | return etch.update(this) 41 | } 42 | 43 | public writeAfterUpdate() { 44 | if (this.element.parentElement) { 45 | this.element.parentElement.classList.add('ide-haskell') 46 | } 47 | } 48 | 49 | private renderTooltip() { 50 | if (this.message.length) { 51 | return ( 52 | 56 | {this.message} 57 | 58 | ) 59 | } else { 60 | return null 61 | } 62 | } 63 | 64 | private tooltipClick = (e: MouseEvent) => { 65 | if (!e.target) return 66 | const htmlTarget = e.target as HTMLElement 67 | if (htmlTarget.matches('a')) { 68 | const href = (htmlTarget as HTMLLinkElement).href 69 | if (href) { 70 | handlePromise( 71 | atom.workspace.open(`ide-haskell://hoogle/web/${href}`, { 72 | searchAllPanes: true, 73 | split: 'right', 74 | activateItem: true, 75 | }), 76 | ) 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/tooltip-registry/index.ts: -------------------------------------------------------------------------------- 1 | import { TextEditor, Range, RangeCompatible } from 'atom' 2 | import { MessageObject } from '../utils' 3 | import { PluginManager } from '../plugin-manager' 4 | import * as UPI from 'atom-haskell-upi' 5 | import TEventRangeType = UPI.TEventRangeType 6 | import { PriorityRegistry } from '../priority-registry' 7 | 8 | export interface ITooltipDataExt { 9 | range: RangeCompatible 10 | text: UPI.TSingleOrArray 11 | persistent?: boolean 12 | } 13 | 14 | export class TooltipRegistry extends PriorityRegistry { 15 | constructor(private readonly pluginManager: PluginManager) { 16 | super() 17 | } 18 | 19 | public async defaultTooltipFunction( 20 | editor: TextEditor, 21 | type: TEventRangeType, 22 | crange: Range, 23 | ) { 24 | for (const { pluginName, handler, eventTypes } of this.providers) { 25 | if (!eventTypes.includes(type)) { 26 | continue 27 | } 28 | const awaiter = this.pluginManager.getAwaiter(pluginName) 29 | const tooltipData = await awaiter(() => handler(editor, crange, type)) 30 | if (tooltipData === undefined) continue 31 | return { pluginName, tooltipData } 32 | } 33 | return undefined 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/upi-3/consume.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, Disposable } from 'atom' 2 | 3 | import { PluginManager } from '../plugin-manager' 4 | import { MAIN_MENU_LABEL, handlePromise } from '../utils' 5 | import * as UPI from 'atom-haskell-upi' 6 | import TEventRangeType = UPI.TEventRangeType 7 | import { Provider } from '../results-db/provider' 8 | 9 | export interface FeatureSet { 10 | eventsReturnResults?: boolean 11 | supportsCommands?: boolean 12 | supportsActions?: boolean 13 | } 14 | 15 | export function consume( 16 | pluginManager: PluginManager, 17 | options: UPI.IRegistrationOptions, 18 | featureSet: FeatureSet, 19 | ): Disposable { 20 | const { 21 | name, 22 | menu, 23 | messageTypes, 24 | events, 25 | controls, 26 | params, 27 | tooltip, 28 | commands, 29 | actions, 30 | } = options 31 | const disp = new CompositeDisposable() 32 | let messageProvider: Provider | undefined 33 | 34 | function registerEvent( 35 | cb: UPI.TSingleOrArray, 36 | reg: (cb: UPI.TTextBufferCallback) => Disposable, 37 | ) { 38 | if (Array.isArray(cb)) { 39 | const disp = new CompositeDisposable() 40 | for (const i of cb) { 41 | disp.add(reg(wrapStatus(i))) 42 | } 43 | return disp 44 | } else { 45 | return reg(wrapStatus(cb)) 46 | } 47 | } 48 | 49 | const awaiter = pluginManager.getAwaiter(name) 50 | 51 | function wrapStatus>( 52 | cb: (...args: Args) => ReturnType, 53 | ) { 54 | return function(...args: Args): void { 55 | handlePromise( 56 | awaiter(() => cb(...args)).then(async (res) => { 57 | if (messageProvider && res !== undefined) { 58 | if (Array.isArray(res)) { 59 | messageProvider.setMessages(res) 60 | } else { 61 | const items = [] 62 | for await (const item of res) { 63 | items.push(item) 64 | messageProvider.setMessages(items) 65 | } 66 | } 67 | } 68 | }), 69 | ) 70 | } 71 | } 72 | 73 | if (menu) { 74 | const menuDisp = atom.menu.add([ 75 | { 76 | label: MAIN_MENU_LABEL, 77 | submenu: [{ label: menu.label, submenu: menu.menu }], 78 | }, 79 | ]) 80 | disp.add(menuDisp) 81 | } 82 | if (messageTypes) { 83 | if (featureSet.eventsReturnResults) { 84 | messageProvider = pluginManager.resultsDB.registerProvider( 85 | Object.keys(messageTypes), 86 | ) 87 | } 88 | for (const type of Object.keys(messageTypes)) { 89 | const opts = messageTypes[type] 90 | handlePromise(pluginManager.outputPanel.createTab(type, opts)) 91 | disp.add( 92 | new Disposable(function() { 93 | handlePromise(pluginManager.outputPanel.removeTab(type)) 94 | }), 95 | ) 96 | } 97 | } 98 | if (events) { 99 | if (events.onWillSaveBuffer) { 100 | disp.add( 101 | registerEvent(events.onWillSaveBuffer, pluginManager.onWillSaveBuffer), 102 | ) 103 | } 104 | if (events.onDidSaveBuffer) { 105 | disp.add( 106 | registerEvent(events.onDidSaveBuffer, pluginManager.onDidSaveBuffer), 107 | ) 108 | } 109 | if (events.onDidStopChanging) { 110 | disp.add( 111 | registerEvent( 112 | events.onDidStopChanging, 113 | pluginManager.onDidStopChanging, 114 | ), 115 | ) 116 | } 117 | } 118 | if (tooltip) { 119 | let handler: UPI.TTooltipHandler 120 | let priority: number | undefined 121 | let eventTypes: TEventRangeType[] | undefined 122 | if (typeof tooltip === 'function') { 123 | handler = tooltip 124 | } else { 125 | ;({ handler, priority, eventTypes } = tooltip) 126 | } 127 | disp.add( 128 | pluginManager.tooltipRegistry.register(name, { 129 | priority: priority ?? 100, 130 | handler, 131 | eventTypes, 132 | }), 133 | ) 134 | } 135 | if (controls) { 136 | for (const i of controls) { 137 | disp.add(pluginManager.outputPanel.addPanelControl(i)) 138 | } 139 | } 140 | if (params) { 141 | for (const paramName of Object.keys(params)) { 142 | const spec = params[paramName] 143 | disp.add(pluginManager.configParamManager.add(name, paramName, spec)) 144 | } 145 | } 146 | if (featureSet.supportsCommands && commands) { 147 | for (const [target, cmds] of Object.entries(commands)) { 148 | if (cmds === undefined) continue 149 | for (const [cmd, handler] of Object.entries(cmds)) { 150 | disp.add( 151 | atom.commands.add(target, cmd, function(event) { 152 | wrapStatus(handler)(event.currentTarget) 153 | }), 154 | ) 155 | } 156 | } 157 | } 158 | if (featureSet.supportsActions && actions) { 159 | let handler: UPI.TActionHandler 160 | let priority: number | undefined 161 | let eventTypes: TEventRangeType[] | undefined 162 | if (typeof actions === 'function') { 163 | handler = actions 164 | } else { 165 | ;({ handler, priority, eventTypes } = actions) 166 | } 167 | disp.add( 168 | pluginManager.actionRegistry.register(name, { 169 | priority: priority ?? 100, 170 | handler: async function(editor, range, types) { 171 | const actions = await Promise.resolve(handler(editor, range, types)) 172 | if (!actions) return undefined 173 | if (!actions.length) return undefined 174 | return actions 175 | }, 176 | eventTypes, 177 | }), 178 | ) 179 | } 180 | 181 | return disp 182 | } 183 | -------------------------------------------------------------------------------- /src/upi-3/index.ts: -------------------------------------------------------------------------------- 1 | export * from './instance' 2 | export * from './consume' 3 | -------------------------------------------------------------------------------- /src/upi-3/instance.ts: -------------------------------------------------------------------------------- 1 | import { MAIN_MENU_LABEL, getEventType, isTEventRangeType } from '../utils' 2 | import { PluginManager } from '../plugin-manager' 3 | import { consume, FeatureSet } from './consume' 4 | import * as UPI from 'atom-haskell-upi' 5 | import * as AtomTypes from 'atom' 6 | import CompositeDisposable = AtomTypes.CompositeDisposable 7 | type TextEditor = AtomTypes.TextEditor 8 | type TEventRangeType = UPI.TEventRangeType 9 | 10 | export function instance( 11 | pluginManager: PluginManager, 12 | options: UPI.IRegistrationOptions, 13 | featureSet: FeatureSet, 14 | ): UPI.IUPIInstance { 15 | const pluginName = options.name 16 | const disposables = new CompositeDisposable() 17 | const messageProvider = pluginManager.resultsDB.registerProvider( 18 | options.messageTypes !== undefined ? Object.keys(options.messageTypes) : [], 19 | ) 20 | disposables.add(messageProvider) 21 | disposables.add(consume(pluginManager, options, featureSet)) 22 | 23 | return { 24 | setMenu(name: string, menu: ReadonlyArray) { 25 | const menuDisp = atom.menu.add([ 26 | { 27 | label: MAIN_MENU_LABEL, 28 | submenu: [{ label: name, submenu: menu }], 29 | }, 30 | ]) 31 | disposables.add(menuDisp) 32 | return menuDisp 33 | }, 34 | setStatus(status: UPI.IStatus) { 35 | return pluginManager.forceBackendStatus(pluginName, status) 36 | }, 37 | setMessages(messages: UPI.IResultItem[]) { 38 | messageProvider.setMessages(messages) 39 | }, 40 | async addMessageTab(name: string, opts: UPI.ISeverityTabDefinition) { 41 | return pluginManager.outputPanel.createTab(name, opts) 42 | }, 43 | async showTooltip({ 44 | editor, 45 | eventType, 46 | detail, 47 | tooltip, 48 | }: UPI.IShowTooltipParams) { 49 | if (eventType === undefined) { 50 | eventType = getEventType(detail) 51 | } 52 | return pluginManager.tooltipManager.showTooltip(editor, eventType, { 53 | pluginName, 54 | tooltip, 55 | }) 56 | }, 57 | addPanelControl(def: UPI.TControlDefinition) { 58 | return pluginManager.outputPanel.addPanelControl(def) 59 | }, 60 | addConfigParam(paramName: string, spec: UPI.IParamSpec) { 61 | return pluginManager.configParamManager.add(pluginName, paramName, spec) 62 | }, 63 | async getConfigParam(name: string): Promise { 64 | return pluginManager.configParamManager.get(pluginName, name) 65 | }, 66 | async getOthersConfigParam( 67 | plugin: string, 68 | name: string, 69 | ): Promise { 70 | return pluginManager.configParamManager.get(plugin, name) 71 | }, 72 | async setConfigParam(name: string, value?: T): Promise { 73 | return pluginManager.configParamManager.set(pluginName, name, value) 74 | }, 75 | getEventRange(editor: TextEditor, typeOrDetail: TEventRangeType | Object) { 76 | let type: TEventRangeType 77 | if (isTEventRangeType(typeOrDetail)) { 78 | type = typeOrDetail 79 | } else { 80 | type = getEventType(typeOrDetail) 81 | } 82 | const controller = pluginManager.controller(editor) 83 | if (!controller) { 84 | return undefined 85 | } 86 | return controller.getEventRange(type) 87 | }, 88 | dispose() { 89 | disposables.dispose() 90 | }, 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/utils/cast.ts: -------------------------------------------------------------------------------- 1 | import * as AtomTypes from 'atom' 2 | import * as UPI from 'atom-haskell-upi' 3 | import TEventRangeType = UPI.TEventRangeType 4 | type Dock = AtomTypes.Dock 5 | type WorkspaceCenter = AtomTypes.WorkspaceCenter 6 | 7 | export function isDock(object: Dock | WorkspaceCenter): object is Dock { 8 | return object.constructor.name === 'Dock' 9 | } 10 | 11 | export function isSimpleControlDef( 12 | def: UPI.TControlDefinition, 13 | ): def is UPI.IControlSimpleDefinition { 14 | return typeof def.element === 'string' 15 | } 16 | 17 | export function notUndefined(val: T | undefined): val is T { 18 | return val !== undefined 19 | } 20 | 21 | export const eventRangeTypeVals = [ 22 | TEventRangeType.context, 23 | TEventRangeType.keyboard, 24 | TEventRangeType.mouse, 25 | TEventRangeType.selection, 26 | ] 27 | 28 | export function isTEventRangeType( 29 | x: TEventRangeType | Object, 30 | ): x is TEventRangeType { 31 | return ( 32 | typeof x === 'string' && eventRangeTypeVals.includes(x as TEventRangeType) 33 | ) 34 | } 35 | 36 | export function isTextMessage(msg: UPI.TMessage): msg is UPI.IMessageText { 37 | return !!(msg && (msg as UPI.IMessageText).text) 38 | } 39 | 40 | export function isHTMLMessage(msg: UPI.TMessage): msg is UPI.IMessageHTML { 41 | return !!(msg && (msg as UPI.IMessageHTML).html) 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/element-listener.ts: -------------------------------------------------------------------------------- 1 | import { Disposable } from 'atom' 2 | 3 | export function listen( 4 | element: HTMLElement, 5 | event: T, 6 | selector: string, 7 | callback: (event: HTMLElementEventMap[T]) => void, 8 | ): Disposable { 9 | const bound = (evt: HTMLElementEventMap[T]) => { 10 | const sel = (evt.target as HTMLElement).closest(selector) 11 | if (sel && element.contains(sel)) { 12 | callback(evt) 13 | } 14 | } 15 | element.addEventListener(event, bound) 16 | return new Disposable(() => { 17 | element.removeEventListener(event, bound) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { Point, TextEditor } from 'atom' 2 | import * as UPI from 'atom-haskell-upi' 3 | import TEventRangeType = UPI.TEventRangeType 4 | 5 | export { MessageObject } from './message-object' 6 | export * from './cast' 7 | export * from './element-listener' 8 | 9 | export const MAIN_MENU_LABEL = 'Haskell IDE' 10 | 11 | export function getEventType(detail: any) { 12 | if ( 13 | detail && 14 | (detail.contextCommand || (detail[0] && detail[0].contextCommand)) 15 | ) { 16 | return TEventRangeType.context 17 | } else { 18 | return TEventRangeType.keyboard 19 | } 20 | } 21 | 22 | // screen position from mouse event 23 | export function bufferPositionFromMouseEvent( 24 | editor: TextEditor, 25 | event: MouseEvent, 26 | ) { 27 | const sp: Point = (atom.views.getView( 28 | editor, 29 | ) as any).component.screenPositionForMouseEvent(event) 30 | if (isNaN(sp.row) || isNaN(sp.column)) { 31 | return undefined 32 | } 33 | return editor.bufferPositionForScreenPosition(sp) 34 | } 35 | 36 | export function handlePromise(promise: Promise): void { 37 | // tslint:disable-next-line:strict-type-predicates no-unbound-method 38 | if (typeof promise.catch !== 'function') { 39 | atom.notifications.addFatalError( 40 | 'IDE-Haskell: non-promise passed to handlePromise. Please report this.', 41 | { 42 | stack: new Error().stack, 43 | dismissable: true, 44 | }, 45 | ) 46 | return 47 | } 48 | promise.catch((err: Error) => { 49 | atom.notifications.addFatalError(`IDE-Haskell error: ${err.message}`, { 50 | detail: err.toString(), 51 | stack: err.stack, 52 | dismissable: true, 53 | }) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/message-object.ts: -------------------------------------------------------------------------------- 1 | import { hightlightLines } from 'atom-highlight' 2 | import * as cast from './cast' 3 | import * as UPI from 'atom-haskell-upi' 4 | import Memoize from 'lodash-decorators/memoize' 5 | 6 | export class MessageObject { 7 | constructor(private msg: UPI.TMessage) { 8 | // noop 9 | } 10 | 11 | public static fromObject = ( 12 | message: UPI.TMessage | MessageObject, 13 | ): MessageObject => { 14 | if (message instanceof MessageObject) { 15 | return message 16 | } else { 17 | return new MessageObject(message) 18 | } 19 | } 20 | 21 | @Memoize() 22 | public toHtml(): string { 23 | if (cast.isTextMessage(this.msg) && this.msg.highlighter !== undefined) { 24 | const html = Array.from( 25 | hightlightLines(this.msg.text.split('\n'), this.msg.highlighter), 26 | ) 27 | if (html.length > 0) return html.join('\n') 28 | 29 | this.msg.highlighter = undefined 30 | return this.toHtml() 31 | } else if (cast.isHTMLMessage(this.msg)) { 32 | return this.msg.html 33 | } else { 34 | let text: string 35 | if (cast.isTextMessage(this.msg)) { 36 | text = this.msg.text 37 | } else { 38 | text = this.msg 39 | } 40 | const div = document.createElement('div') 41 | div.innerText = text 42 | return div.innerHTML 43 | } 44 | } 45 | 46 | @Memoize() 47 | public toPlain(): string { 48 | if (cast.isHTMLMessage(this.msg)) { 49 | const div = document.createElement('div') 50 | div.innerHTML = this.msg.html 51 | return div.innerText 52 | } else { 53 | let text: string 54 | if (cast.isTextMessage(this.msg)) { 55 | text = this.msg.text 56 | } else { 57 | text = this.msg 58 | } 59 | return text 60 | } 61 | } 62 | 63 | public raw(): string { 64 | if (cast.isTextMessage(this.msg)) { 65 | return this.msg.text 66 | } else if (cast.isHTMLMessage(this.msg)) { 67 | return this.msg.html 68 | } else { 69 | return this.msg 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /styles/icon.less: -------------------------------------------------------------------------------- 1 | .icon() { 2 | font-family: 'Octicons Regular'; 3 | font-weight: normal; 4 | font-style: normal; 5 | display: inline-block; 6 | -webkit-font-smoothing: antialiased; 7 | text-decoration: none; 8 | } 9 | -------------------------------------------------------------------------------- /styles/ide-haskell-decorations.less: -------------------------------------------------------------------------------- 1 | @import 'octicon-utf-codes'; 2 | @import 'ui-variables'; 3 | @import 'icon'; 4 | 5 | .highlight { 6 | &.ide-haskell-lint .region { 7 | background: linear-gradient( 8 | to right, 9 | @background-color-info 0%, 10 | fadeout(@background-color-info, 50%) 100% 11 | ); 12 | } 13 | &.ide-haskell-warning .region { 14 | background: linear-gradient( 15 | to right, 16 | @background-color-warning 0%, 17 | fadeout(@background-color-warning, 50%) 100% 18 | ); 19 | } 20 | &.ide-haskell-error .region { 21 | background: linear-gradient( 22 | to right, 23 | @background-color-error 0%, 24 | fadeout(@background-color-error, 50%) 100% 25 | ); 26 | } 27 | &.ide-haskell-type .region { 28 | background: fade(@background-color-info, 10%); 29 | } 30 | } 31 | 32 | .gutter[gutter-name='ide-haskell-check-results'] .decoration { 33 | &:before { 34 | .icon; 35 | font-size: 0.8em; 36 | width: 0.8em; 37 | height: 0.8em; 38 | } 39 | 40 | &.ide-haskell-lint { 41 | visibility: visible; 42 | &:before { 43 | content: @info; 44 | color: @text-color-info; 45 | z-index: 1; 46 | } 47 | } 48 | 49 | &.ide-haskell-warning { 50 | visibility: visible; 51 | &:before { 52 | content: @stop; 53 | color: @text-color-warning; 54 | z-index: 2; 55 | } 56 | } 57 | 58 | &.ide-haskell-error { 59 | visibility: visible; 60 | &:before { 61 | content: @alert; 62 | color: @text-color-error; 63 | z-index: 3; 64 | } 65 | } 66 | } 67 | 68 | // A hack to catch mouse events only on .scroll-view 69 | .scroll-view > .lines { 70 | pointer-events: none; 71 | } 72 | -------------------------------------------------------------------------------- /styles/ide-haskell.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/stylesheets/ui-variables.less 4 | // for a full listing of what's available. 5 | @import 'octicon-utf-codes'; 6 | @import 'ui-variables'; 7 | @import 'icon'; 8 | 9 | @font-face { 10 | font-family: 'icomoon'; 11 | src: url('atom://ide-haskell/resources/icomoon.woff') format('woff'); 12 | font-weight: normal; 13 | font-style: normal; 14 | } 15 | 16 | .clickable() { 17 | flex-shrink: 0; 18 | flex-grow: 0; 19 | display: inline-block; 20 | line-height: 1.8em; 21 | color: @text-color-subtle; 22 | border: 1px solid @button-border-color; 23 | background: fade(@button-background-color, 33%); 24 | cursor: pointer; 25 | vertical-align: middle; 26 | 27 | &:active { 28 | background: transparent; 29 | } 30 | } 31 | 32 | .status-icon() { 33 | &:before { 34 | .icon; 35 | font-size: 1.2em; 36 | margin: 0 0.5em; 37 | content: @check; 38 | } 39 | 40 | &[data-status='progress']:before { 41 | content: @hourglass; 42 | } 43 | &[data-status='ready']:before { 44 | content: @check; 45 | } 46 | &[data-status='error']:before { 47 | color: @text-color-error; 48 | content: @alert; 49 | } 50 | &[data-status='warning']:before { 51 | content: @stop; 52 | color: @text-color-warning; 53 | } 54 | } 55 | 56 | ide-haskell-panel { 57 | display: flex; 58 | 59 | &.buttons-top { 60 | width: auto !important; 61 | flex-direction: column; 62 | ide-haskell-panel-heading { 63 | flex-direction: row; 64 | ide-haskell-panel-buttons { 65 | flex-direction: row; 66 | ide-haskell-button { 67 | margin-right: -1px; // hide left border 68 | &:first-of-type { 69 | border-top-left-radius: @component-border-radius; 70 | border-bottom-left-radius: @component-border-radius; 71 | } 72 | &:last-of-type { 73 | margin-right: 0.6em; 74 | border-top-right-radius: @component-border-radius; 75 | border-bottom-right-radius: @component-border-radius; 76 | } 77 | } 78 | } 79 | } 80 | } 81 | &.buttons-left { 82 | height: auto !important; 83 | flex-direction: row; 84 | ide-haskell-panel-heading { 85 | flex-direction: column; 86 | ide-haskell-panel-buttons { 87 | flex-direction: column; 88 | ide-haskell-button { 89 | margin-top: -1px; // hide left border 90 | &:first-of-type { 91 | border-top-left-radius: @component-border-radius; 92 | border-top-right-radius: @component-border-radius; 93 | } 94 | &:last-of-type { 95 | margin-bottom: 0.6em; 96 | border-bottom-left-radius: @component-border-radius; 97 | border-bottom-right-radius: @component-border-radius; 98 | } 99 | } 100 | } 101 | progress { 102 | transform-origin: center center; 103 | transform: rotate(-90deg) translateX(-50%); 104 | width: 100%; 105 | } 106 | } 107 | } 108 | 109 | ide-haskell-panel-heading { 110 | display: flex; 111 | flex-direction: row; 112 | flex-shrink: 0; 113 | flex-wrap: wrap; 114 | align-items: center; 115 | 116 | ide-haskell-status-icon { 117 | padding: 0.5ex 0; 118 | margin: 0px 0.5em; 119 | .status-icon(); 120 | } 121 | 122 | ide-haskell-button { 123 | .clickable; 124 | height: 1.7em; 125 | white-space: nowrap; 126 | &.cancel { 127 | width: 2em; 128 | border-radius: @component-border-radius; 129 | margin: 0.5ex; 130 | &:before { 131 | .icon; 132 | display: block; 133 | text-align: center; 134 | font-size: 1.7em; 135 | content: @x; 136 | } 137 | } 138 | } 139 | 140 | ide-haskell-panel-buttons { 141 | display: flex; 142 | ide-haskell-button { 143 | text-transform: capitalize; 144 | padding: 0 0.6em; 145 | 146 | &.active { 147 | color: @text-color-highlight; 148 | background: @button-background-color; 149 | } 150 | 151 | &:before { 152 | content: attr(data-caption); 153 | } 154 | 155 | &:not([data-count='0']):before { 156 | content: attr(data-caption) ' (' attr(data-count) ')'; 157 | } 158 | } 159 | } 160 | ide-haskell-checkbox.ide-haskell-checkbox--uri-filter { 161 | .clickable; 162 | position: relative; 163 | margin: 0.125em; 164 | font-size: 1.2em; 165 | &:before { 166 | font-family: 'icomoon'; 167 | content: '\e930'; 168 | margin: 0 0.5em; 169 | } 170 | &.enabled:before { 171 | content: '\e926'; 172 | } 173 | border-radius: @component-border-radius; 174 | } 175 | } 176 | 177 | ide-haskell-param { 178 | .clickable; 179 | position: relative; 180 | border-radius: @component-border-radius; 181 | font-size: 1.2em; 182 | margin: 0.125em; 183 | &:before { 184 | content: attr(data-display-name) ':'; 185 | } 186 | ide-haskell-param-value { 187 | margin-right: 1ex; 188 | } 189 | &.hidden-value { 190 | &:before { 191 | content: attr(data-display-name); 192 | } 193 | ide-haskell-param-value { 194 | display: none; 195 | } 196 | } 197 | } 198 | 199 | ide-haskell-panel-items { 200 | display: block; 201 | overflow-y: auto; 202 | padding: 3px @component-padding; 203 | height: 100%; 204 | flex-grow: 1; 205 | 206 | ide-haskell-panel-item { 207 | padding: 0.5em 0; 208 | margin: 0; 209 | white-space: normal; 210 | 211 | ide-haskell-item-position { 212 | cursor: pointer; 213 | display: inline-block; 214 | } 215 | 216 | ide-haskell-item-context { 217 | display: inline-block; 218 | font-family: Consolas, monospace; 219 | margin-left: 1em; 220 | } 221 | 222 | ide-haskell-item-description { 223 | display: block; 224 | padding: 0.2em 1em; 225 | color: lighten(@text-color, 15%); 226 | background-color: @tool-panel-background-color; 227 | font-family: Consolas, monospace; 228 | white-space: pre-wrap; 229 | border-radius: 0.5em; 230 | } 231 | } 232 | } 233 | } 234 | 235 | ide-haskell-tooltip-with-actions { 236 | display: flex; 237 | flex-direction: column-reverse; 238 | align-items: flex-start; 239 | 240 | ide-haskell-tooltip-actions { 241 | pointer-events: all; 242 | flex-grow: 0; 243 | flex-shrink: 1; 244 | display: flex; 245 | flex-direction: row; 246 | } 247 | 248 | ide-haskell-tooltip { 249 | pointer-events: all; 250 | flex-grow: 0; 251 | flex-shrink: 0; 252 | &::before { 253 | border-width: 0 0.5ch 0.5ch; 254 | border-style: solid; 255 | border-color: @overlay-border-color transparent; 256 | display: block; 257 | width: 0; 258 | position: absolute; 259 | top: -0.5ch; 260 | left: 0; 261 | height: 0.5ch; 262 | background-size: 50% 100%; 263 | background-repeat: no-repeat; 264 | margin: 0; 265 | padding: 0; 266 | content: ''; 267 | } 268 | & > div { 269 | text-align: left; 270 | // color: @text-color-info; 271 | font-size: 85%; 272 | display: block; 273 | white-space: pre-wrap; 274 | background: none !important; 275 | word-break: break-all; 276 | } 277 | & > div + div { 278 | margin-top: 1em; 279 | } 280 | display: block; 281 | background: @overlay-background-color; 282 | border: solid 2px @overlay-border-color; 283 | padding: 5px; 284 | border-radius: 0 0.5em; 285 | &.ide-haskell-tooltip-subdued { 286 | opacity: 0.3; 287 | } 288 | } 289 | } 290 | 291 | .select-list.ide-haskell { 292 | .select-list-heading { 293 | color: @text-color-highlight; 294 | font-size: 150%; 295 | text-align: center; 296 | } 297 | .list-group.ide-haskell.mark-active > li { 298 | display: flex; 299 | align-items: center; 300 | & > div { 301 | flex-grow: 1; 302 | } 303 | } 304 | } 305 | 306 | .ide-haskell-status-tooltip .tooltip-inner { 307 | display: flex; 308 | flex-direction: column; 309 | align-items: flex-start; 310 | ide-haskell-status-icon { 311 | .status-icon(); 312 | &:before { 313 | margin-left: 0; 314 | } 315 | } 316 | ide-haskell-status-detail { 317 | display: block; 318 | margin-left: 1.7em; 319 | } 320 | ide-haskell-status-item { 321 | display: flex; 322 | flex-direction: column; 323 | align-items: flex-start; 324 | } 325 | } 326 | 327 | // A hack to catch mouse events only on .scroll-view 328 | atom-overlay.ide-haskell { 329 | pointer-events: none; 330 | } 331 | 332 | .status-bar-right .ide-haskell { 333 | padding-bottom: -0.3em; 334 | &:hover span { 335 | border-bottom: solid 1px; 336 | } 337 | ide-haskell-lambda { 338 | -webkit-mask: url('atom://ide-haskell/resources/haskell.svg') no-repeat 50% 339 | 50%; 340 | mask: url('atom://ide-haskell/resources/haskell.svg') no-repeat 50% 50%; 341 | -webkit-mask-size: contain; 342 | mask-size: contain; 343 | background-color: @text-color; 344 | width: 2em; 345 | height: 1em; 346 | display: inline-block; 347 | margin-bottom: -0.1em; 348 | } 349 | ide-haskell-status-icon { 350 | .status-icon(); 351 | margin-right: 0; 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "allowSyntheticDefaultImports": true, 6 | "moduleResolution": "node", 7 | "isolatedModules": false, 8 | "jsx": "react", 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "declaration": false, 12 | "noImplicitAny": true, 13 | "noImplicitThis": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitUseStrict": false, 16 | "removeComments": true, 17 | "noLib": false, 18 | "jsxFactory": "etch.dom", 19 | "preserveConstEnums": true, 20 | "suppressImplicitAnyIndexErrors": true, 21 | "noEmit": false, 22 | "outDir": "lib", 23 | "strictNullChecks": true, 24 | "allowJs": false, 25 | "lib": ["dom", "esnext"], 26 | "strict": true, 27 | "importHelpers": true, 28 | "noFallthroughCasesInSwitch": true, 29 | "noImplicitReturns": true, 30 | "noUnusedLocals": true, 31 | "noUnusedParameters": true, 32 | "typeRoots": ["./typings", "./node_modules/@types"], 33 | "plugins": [{ "name": "typescript-tslint-plugin" }], 34 | "newLine": "LF", 35 | "skipLibCheck": true 36 | }, 37 | "exclude": ["node_modules", "spec"], 38 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js"], 39 | "compileOnSave": false 40 | } 41 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "atom-haskell-tslint-rules", 3 | "rules": { 4 | "strict-type-predicates": true, 5 | "strict-boolean-expressions": [ 6 | true, 7 | "allow-null-union", 8 | "allow-undefined-union", 9 | "allow-string", 10 | "allow-number", 11 | "allow-boolean-or-undefined" 12 | ] 13 | } 14 | } 15 | --------------------------------------------------------------------------------