├── .gitignore ├── .travis.yml ├── Commands └── Default.sublime-commands ├── Completions ├── Haskell.sublime-completions └── LanguageExtensions.sublime-completions ├── Keymaps ├── Default (Linux).sublime-keymap └── Default (OSX).sublime-keymap ├── LINKS.txt ├── Main.sublime-menu ├── README.md ├── Snippets ├── Data.sublime-snippet ├── DoubleImport.sublime-snippet ├── IfThenElse.sublime-snippet ├── Inline.sublime-snippet ├── Language.sublime-snippet ├── Newtype.sublime-snippet ├── Qualified.sublime-snippet └── Record.sublime-snippet ├── SublimeStackIDE.sublime-settings ├── Syntaxes ├── Cabal.tmLanguage ├── CabalComments.tmPreferences ├── Comments.tmPreferences └── Haskell.tmLanguage ├── TODO.txt ├── __init__.py ├── application_commands.py ├── event_listeners.py ├── log.py ├── req.py ├── response.py ├── settings.py ├── stack_ide.py ├── stack_ide_manager.py ├── test ├── __init__.py ├── data.py ├── fakebackend.py ├── mocks.py ├── projects │ ├── cabal_project │ │ └── cabal_project.cabal │ ├── cabalfile_wrong_project │ │ ├── cabal_project.cabal │ │ └── stack.yaml │ ├── empty_project │ │ └── .empty │ ├── helloworld │ │ ├── LICENSE │ │ ├── Setup.hs │ │ ├── helloworld.cabal │ │ ├── src │ │ │ └── Main.hs │ │ └── stack.yaml │ └── stack_project │ │ ├── stack.yaml │ │ └── stack_project.cabal ├── stubs │ ├── __init__.py │ ├── sublime.py │ └── sublime_plugin.py ├── test_commands.py ├── test_listeners.py ├── test_response.py ├── test_stack_ide_manager.py ├── test_stackide.py ├── test_utility.py └── test_win.py ├── text_commands.py ├── utility.py ├── watchdog.py ├── win.py └── window_commands.py /.gitignore: -------------------------------------------------------------------------------- 1 | session.*/ 2 | *.pyc 3 | __pycache__ 4 | htmlcov 5 | *.log 6 | *.coverage 7 | .stack-work 8 | *.sublime-workspace 9 | autocomplete-output.json 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | 5 | # command to install dependencies 6 | install: 7 | - pip install coverage 8 | - pip install coveralls 9 | 10 | # command to run tests 11 | script: coverage run --source="." --omit="test/*" -m unittest 12 | 13 | # publish results 14 | after_success: coveralls -------------------------------------------------------------------------------- /Commands/Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "SublimeStackIDE: Restart", 4 | "command": "restart_stack_ide" 5 | } 6 | , { 7 | "caption": "SublimeStackIDE: Show Type", 8 | "command": "show_hs_type_at_cursor" 9 | } 10 | , { 11 | "caption": "SublimeStackIDE: Show Info", 12 | "command": "show_hs_info_at_cursor" 13 | } 14 | , 15 | { 16 | "caption": "SublimeStackIDE: Goto Definition", 17 | "command": "goto_definition_at_cursor" 18 | } 19 | , 20 | { 21 | "caption": "SublimeStackIDE: Copy Type to Clipboard", 22 | "command": "copy_hs_type_at_cursor" 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /Completions/Haskell.sublime-completions: -------------------------------------------------------------------------------- 1 | { 2 | "scope": "source.haskell", 3 | 4 | "completions": 5 | [ 6 | "class", 7 | "default", 8 | "deriving", 9 | "do", 10 | "forall", 11 | "foreign", 12 | "hiding", 13 | "infix", 14 | "infixl", 15 | "infixr", 16 | "instance", 17 | "let", 18 | "mdo", 19 | "module", 20 | "newtype", 21 | "qualified", 22 | "rec", 23 | "type", 24 | "undefined", 25 | "where", 26 | // Pragmas, see http://www.haskell.org/ghc/docs/latest/html/users_guide/pragmas.html 27 | "CONLIKE", 28 | "DEPRECATED", 29 | "INCLUDE", 30 | "INLINABLE", 31 | "INLINE", 32 | "LANGUAGE", 33 | "LINE", 34 | "NOINLINE", 35 | "OPTIONS_GHC", 36 | "RULES", 37 | "SOURCE", 38 | "SPECIALIZE", 39 | "UNPACK", 40 | "WARNING", 41 | // Others 42 | "ANN", 43 | { "trigger": "import", "contents": "import $0" } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /Completions/LanguageExtensions.sublime-completions: -------------------------------------------------------------------------------- 1 | { 2 | "scope": "meta.preprocessor.haskell", 3 | 4 | "completions": 5 | [ 6 | "AllowAmbiguousTypes", 7 | "AlternativeLayoutRule", 8 | "AlternativeLayoutRuleTransitional", 9 | "Arrows", 10 | "AutoDeriveTypeable", 11 | "BangPatterns", 12 | "BinaryLiterals", 13 | "CApiFFI", 14 | "ConstrainedClassMethods", 15 | "ConstraintKinds", 16 | "CPP", 17 | "DataKinds", 18 | "DatatypeContexts", 19 | "DefaultSignatures", 20 | "DeriveAnyClass", 21 | "DeriveDataTypeable", 22 | "DeriveFoldable", 23 | "DeriveFunctor", 24 | "DeriveGeneric", 25 | "DeriveTraversable", 26 | "DisambiguateRecordFields", 27 | "DoAndIfThenElse", 28 | "DoRec", 29 | "EmptyCase", 30 | "EmptyDataDecls", 31 | "ExistentialQuantification", 32 | "ExplicitForAll", 33 | "ExplicitNamespaces", 34 | "ExtendedDefaultRules", 35 | "FlexibleContexts", 36 | "FlexibleInstances", 37 | "ForeignFunctionInterface", 38 | "FunctionalDependencies", 39 | "GADTs", 40 | "GADTSyntax", 41 | "GeneralizedNewtypeDeriving", 42 | "Generics", 43 | "GHCForeignImportPrim", 44 | "Haskell2010", 45 | "Haskell98", 46 | "ImplicitParams", 47 | "ImplicitPrelude", 48 | "ImpredicativeTypes", 49 | "IncoherentInstances", 50 | "InstanceSigs", 51 | "InterruptibleFFI", 52 | "JavaScriptFFI", 53 | "KindSignatures", 54 | "LambdaCase", 55 | "LiberalTypeSynonyms", 56 | "MagicHash", 57 | "MonadComprehensions", 58 | "MonoLocalBinds", 59 | "MonomorphismRestriction", 60 | "MonoPatBinds", 61 | "MultiParamTypeClasses", 62 | "MultiWayIf", 63 | "NamedFieldPuns", 64 | "NamedWildCards", 65 | "NegativeLiterals", 66 | "NPlusKPatterns", 67 | "NullaryTypeClasses", 68 | "NumDecimals", 69 | "OverlappingInstances", 70 | "OverloadedLists", 71 | "OverloadedStrings", 72 | "PackageImports", 73 | "ParallelArrays", 74 | "ParallelListComp", 75 | "PartialTypeSignatures", 76 | "PatternGuards", 77 | "PatternSignatures", 78 | "PatternSynonyms", 79 | "PolyKinds", 80 | "PolymorphicComponents", 81 | "PostfixOperators", 82 | "QuasiQuotes", 83 | "Rank2Types", 84 | "RankNTypes", 85 | "RebindableSyntax", 86 | "RecordPuns", 87 | "RecordWildCards", 88 | "RecursiveDo", 89 | "RelaxedLayout", 90 | "RelaxedPolyRec", 91 | "RoleAnnotations", 92 | "Safe", 93 | "ScopedTypeVariables", 94 | "StandaloneDeriving", 95 | "StaticPointers", 96 | "TemplateHaskell", 97 | "TraditionalRecordSyntax", 98 | "TransformListComp", 99 | "Trustworthy", 100 | "TupleSections", 101 | "TypeFamilies", 102 | "TypeOperators", 103 | "TypeSynonymInstances", 104 | "UnboxedTuples", 105 | "UndecidableInstances", 106 | "UnicodeSyntax", 107 | "UnliftedFFITypes", 108 | "Unsafe", 109 | "ViewPatterns" 110 | ] 111 | } 112 | 113 | -------------------------------------------------------------------------------- /Keymaps/Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+e"], "command": "show_panel", "args": {"panel": "output.hide_errors"} }, 3 | { "keys": ["ctrl+i"], "command": "show_hs_info_at_cursor", "context": [ 4 | {"key": "selector", "operator": "equal", "operand": "source.haskell"} 5 | ] 6 | } 7 | , { "keys": ["ctrl+d"], "command": "goto_definition_at_cursor", "context": [ 8 | {"key": "selector", "operator": "equal", "operand": "source.haskell"} 9 | ] 10 | } 11 | , { 12 | "keys": ["ctrl+t","ctrl+t"], "command": "show_hs_type_at_cursor", "context": [ 13 | {"key": "selector", "operator": "equal", "operand": "source.haskell"} 14 | ] 15 | } 16 | , { 17 | "keys": ["ctrl+t","ctrl+y"], "command": "copy_hs_type_at_cursor", "context": [ 18 | {"key": "selector", "operator": "equal", "operand": "source.haskell"} 19 | ] 20 | } 21 | ] 22 | -------------------------------------------------------------------------------- /Keymaps/Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | 3 | { "keys": ["ctrl+shift+e"], "command": "show_panel", "args": {"panel": "output.hide_errors"} }, 4 | { "keys": ["ctrl+shift+i"], "command": "show_hs_info_at_cursor", "context": [ 5 | {"key": "selector", "operator": "equal", "operand": "source.haskell"} 6 | ] 7 | } 8 | , { "keys": ["ctrl+shift+d"], "command": "goto_definition_at_cursor", "context": [ 9 | {"key": "selector", "operator": "equal", "operand": "source.haskell"} 10 | ] 11 | } 12 | , { 13 | "keys": ["super+t","super+t"], "command": "show_hs_type_at_cursor", "context": [ 14 | {"key": "selector", "operator": "equal", "operand": "source.haskell"} 15 | ] 16 | } 17 | , { 18 | "keys": ["super+t","super+y"], "command": "copy_hs_type_at_cursor", "context": [ 19 | {"key": "selector", "operator": "equal", "operand": "source.haskell"} 20 | ] 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /LINKS.txt: -------------------------------------------------------------------------------- 1 | General plugin info: 2 | http://docs.sublimetext.info/en/latest/extensibility/plugins.html 3 | 4 | 5 | The official, but super incomplete, API reference: 6 | 7 | http://www.sublimetext.com/docs/3/api_reference.html 8 | 9 | Docs on many hidden APIs: 10 | http://www.sublimetext.com/3 (for ST3+) 11 | http://www.sublimetext.com/dev (for ST2 development) 12 | 13 | Documentation for the autocomplete API: 14 | 15 | http://sublime-text-unofficial-documentation.readthedocs.org/en/latest/reference/api.html#sublime_plugin.EventListener.on_query_completions 16 | 17 | Documents the trigger/contents pair for annotating completions with \t 18 | http://sublime-text-unofficial-documentation.readthedocs.org/en/latest/reference/completions.html#completions-trigger-based 19 | 20 | 21 | 22 | 23 | 24 | Documentation for the HTML tooltip API: 25 | https://www.sublimetext.com/forum/viewtopic.php?f=2&t=17583&start=40 26 | 27 | 28 | Views have a "settings" property, which can be used for stashing data. 29 | view.settings().set("custom_id", uuid.uuid4()) -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Preferences", 4 | "id": "preferences", 5 | "mnemonic": "n", 6 | "children": 7 | [ 8 | { 9 | "caption": "Package Settings", 10 | "id": "package-settings", 11 | "mnemonic": "P", 12 | "children": 13 | [ 14 | { 15 | "caption": "SublimeStackIDE", 16 | "children": 17 | [ 18 | { 19 | "caption": "README", 20 | "command": "open_file", "args": 21 | { 22 | "file": "${packages}/SublimeStackIDE/README.md" 23 | } 24 | }, 25 | { 26 | "caption": "-" 27 | }, 28 | { 29 | "caption": "Settings – Default", 30 | "command": "open_file", "args": 31 | { 32 | "file": "${packages}/SublimeStackIDE/SublimeStackIDE.sublime-settings" 33 | } 34 | }, 35 | { 36 | "caption": "Settings – User", 37 | "command": "open_file", "args": 38 | { 39 | "file": "${packages}/User/SublimeStackIDE.sublime-settings" 40 | } 41 | } 42 | ] 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | ] 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![build status](https://travis-ci.org/lukexi/stack-ide-sublime.svg) 3 | [![Coverage Status](https://coveralls.io/repos/lukexi/stack-ide-sublime/badge.svg?branch=master&service=github)](https://coveralls.io/github/lukexi/stack-ide-sublime?branch=master) 4 | 5 | # stack-ide-sublime 6 | 7 | Sublime Text plugin for [stack-ide](https://github.com/commercialhaskell/stack-ide) 8 | 9 | **Bleeding edge note:** 10 | Requires `stack` 0.1.6+, `stack-ide` 0.1+, `ide-backend` HEAD and GHC 7.10+. 11 | 12 | `stack-ide-sublime` also requires for the moment that you are opening the same folder that holds the `.cabal` file, and that the folder is named the same as the `.cabal` file. 13 | 14 | ### Install instructions 15 | 16 | First make sure to install [stack](https://github.com/commercialhaskell/stack#user-content-how-to-install) 17 | and [stack-ide](https://github.com/commercialhaskell/stack-ide). 18 | 19 | **On OSX** install this package with the following command: 20 | `(cd "~/Library/Application Support/Sublime Text 3/Packages"; git clone https://github.com/lukexi/stack-ide-sublime.git SublimeStackIDE)` 21 | 22 | **On Linux** install this package with the following command: 23 | `(cd ~/.config/sublime-text-3/Packages; git clone https://github.com/lukexi/stack-ide-sublime.git SublimeStackIDE)` 24 | 25 | **On Windows** install this package with the following command: 26 | `(cd $APPDATA/Sublime\ Text\ 3/Packages/; git clone https://github.com/lukexi/stack-ide-sublime.git SublimeStackIDE)` 27 | 28 | 29 | ### Screenshots 30 | 31 | ![SublimeStackIDE Errors](http://lukexi.github.io/RawhideErrors.png) 32 | ![SublimeStackIDE Autocomplete](http://lukexi.github.io/RawhideAutocomplete.png) 33 | ![SublimeStackIDE Type-at-cursor](http://lukexi.github.io/RawhideTypeAtCursor.png) 34 | 35 | 36 | ### Tips 37 | 38 | #### Hide stack-ide generated folders from Sublime Text 39 | 40 | Add the following to your global User Preferences *(Sublime Text -> Preferences -> Settings - User)*: 41 | 42 | `"folder_exclude_patterns": [".git", ".svn", "CVS", ".stack-work", "session.*"],` 43 | 44 | 45 | ### Troubleshooting 46 | 47 | First check the Sublime Text console with `ctrl-``. You can increase the plugin's log level by changing the "verbosity" setting in SublimeStackIDE.sublime-settings to "debug". Let us know what you see and we'll get it fixed. 48 | 49 | #### Known issues 50 | 51 | ##### Not working in executable targets 52 | 53 | Add modules (eg. Main) to the executable target's `other-modules` list in the cabal file. 54 | ``` 55 | executable helloworld-exe 56 | hs-source-dirs: app 57 | other-modules: Main 58 | main-is: Main.hs 59 | ``` 60 | 61 | After restarting Stack IDE you should see the listed modules being compiled (see https://github.com/commercialhaskell/stack-ide/issues/28) 62 | 63 | ##### Error "can't find file: /Users/myself/first-project/Lib" in the console 64 | 65 | This was a problem in stack 1.3, upgrade to a newer version (see: https://github.com/lukexi/stack-ide-sublime/issues/13) 66 | 67 | -------------------------------------------------------------------------------- /Snippets/Data.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | data 4 | source.haskell 5 | data declaration 6 | 7 | -------------------------------------------------------------------------------- /Snippets/DoubleImport.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 13 | 18 | impq 19 | source.haskell 20 | Import module qualified and unqualified 21 | 22 | -------------------------------------------------------------------------------- /Snippets/IfThenElse.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | if 4 | source.haskell 5 | if-then-else 6 | 7 | -------------------------------------------------------------------------------- /Snippets/Inline.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | inline 4 | source.haskell 5 | Inline Pragma 6 | 7 | -------------------------------------------------------------------------------- /Snippets/Language.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | lang 4 | source.haskell 5 | Language Extension 6 | 7 | -------------------------------------------------------------------------------- /Snippets/Newtype.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 4 | newtype 5 | source.haskell 6 | 7 | -------------------------------------------------------------------------------- /Snippets/Qualified.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | qualified 6 | source.haskell 7 | 8 | -------------------------------------------------------------------------------- /Snippets/Record.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 7 | record 8 | source.haskell 9 | 10 | -------------------------------------------------------------------------------- /SublimeStackIDE.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // List of directories to add to the default system path when running 'stack'. 3 | "add_to_PATH": [] 4 | 5 | // Controls the messages sent to the console. 6 | // Possible values: "none", "error", "warning", "normal", "debug". 7 | ,"verbosity": "warning" 8 | 9 | // If "show_popup" is true, a popup will appear right below selection 10 | // to show the type in addition to the text shown in the status bar 11 | ,"show_popup": false 12 | 13 | // if show_popup is true, clicking on types displayed in the popup panel will 14 | // open a web-browser at specified hoogle_url with the right type query. 15 | // if you set this to your own hoogle webserver setup for your project, 16 | // you'll be able to see the documentation for your own type 17 | // example: "hoogle_url": "https://www.google.fr/search?q=what+is+haskell+" 18 | ,"hoogle_url": "http://www.stackage.org/lts/hoogle?q=" 19 | } 20 | -------------------------------------------------------------------------------- /Syntaxes/Cabal.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | fileTypes 6 | 7 | cabal 8 | 9 | keyEquivalent 10 | ^~C 11 | name 12 | Cabal 13 | patterns 14 | 15 | 16 | begin 17 | ^(\s*)(exposed-modules):$ 18 | beginCaptures 19 | 20 | 2 21 | 22 | name 23 | constant.other.cabal 24 | 25 | 26 | end 27 | ^(?!\1\s) 28 | name 29 | exposed.modules.cabal 30 | patterns 31 | 32 | 33 | include 34 | #module_name 35 | 36 | 37 | 38 | 39 | begin 40 | ^(\s*)(build-depends):$ 41 | beginCaptures 42 | 43 | 2 44 | 45 | name 46 | constant.other.cabal 47 | 48 | 49 | end 50 | ^(?!\1\s) 51 | name 52 | exposed.modules.cabal 53 | patterns 54 | 55 | 56 | match 57 | ([<>=]+)|([&|]+) 58 | name 59 | keyword.operator.haskell 60 | 61 | 62 | match 63 | ((\d+|\*)\.)*(\d+|\*) 64 | name 65 | constant.numeric.haskell 66 | 67 | 68 | match 69 | ([\w\-]+) 70 | name 71 | support.function.haskell 72 | 73 | 74 | 75 | 76 | captures 77 | 78 | 1 79 | 80 | name 81 | constant.other.cabal 82 | 83 | 2 84 | 85 | name 86 | punctuation.entity.cabal 87 | 88 | 89 | match 90 | ^\s*([a-zA-Z_-]+)(:)\s+ 91 | name 92 | entity.cabal 93 | 94 | 95 | captures 96 | 97 | 1 98 | 99 | name 100 | keyword.entity.cabal 101 | 102 | 2 103 | 104 | name 105 | string.cabal 106 | 107 | 108 | match 109 | ^(?i)(executable|library|test-suite|benchmark|flag|source-repository)\s+([^\s,]+)\s*$ 110 | name 111 | entity.cabal 112 | 113 | 114 | match 115 | ^(?i)library\s*$ 116 | name 117 | keyword.entity.cabal 118 | 119 | 120 | match 121 | --.*$ 122 | name 123 | comment.cabal 124 | 125 | 126 | repository 127 | 128 | module_name 129 | 130 | match 131 | ([A-Z][A-Za-z_'0-9]*)(\.[A-Z][A-Za-z_'0-9]*)* 132 | name 133 | storage.module.haskell 134 | 135 | 136 | scopeName 137 | source.cabal 138 | uuid 139 | 22F622A3-4F1D-4D66-999F-10D42FBFFC4D 140 | 141 | 142 | -------------------------------------------------------------------------------- /Syntaxes/CabalComments.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Comments 7 | scope 8 | source.cabal 9 | settings 10 | 11 | shellVariables 12 | 13 | 14 | name 15 | TM_COMMENT_START 16 | value 17 | -- 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Syntaxes/Comments.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Comments 7 | scope 8 | source.haskell 9 | settings 10 | 11 | shellVariables 12 | 13 | 14 | name 15 | TM_COMMENT_START_2 16 | value 17 | {- 18 | 19 | 20 | name 21 | TM_COMMENT_END_2 22 | value 23 | -} 24 | 25 | 26 | name 27 | TM_COMMENT_START 28 | value 29 | -- 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Syntaxes/Haskell.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | fileTypes 6 | 7 | hs 8 | hsc 9 | 10 | keyEquivalent 11 | ^~H 12 | name 13 | Haskell 14 | patterns 15 | 16 | 17 | captures 18 | 19 | 1 20 | 21 | name 22 | punctuation.definition.entity.haskell 23 | 24 | 2 25 | 26 | name 27 | punctuation.definition.entity.haskell 28 | 29 | 30 | comment 31 | In case this regex seems unusual for an infix operator, note that Haskell allows any ordinary function application (elem 4 [1..10]) to be rewritten as an infix expression (4 `elem` [1..10]). 32 | match 33 | (`)[a-zA-Z_']*?(`) 34 | name 35 | keyword.operator.function.infix.haskell 36 | 37 | 38 | match 39 | \(\) 40 | name 41 | constant.language.unit.haskell 42 | 43 | 44 | match 45 | \[\] 46 | name 47 | constant.language.empty-list.haskell 48 | 49 | 50 | begin 51 | \b(module)\b 52 | beginCaptures 53 | 54 | 1 55 | 56 | name 57 | keyword.other.haskell 58 | 59 | 60 | end 61 | \b(where)\b 62 | endCaptures 63 | 64 | 1 65 | 66 | name 67 | keyword.other.haskell 68 | 69 | 70 | name 71 | meta.declaration.module.haskell 72 | patterns 73 | 74 | 75 | include 76 | #module_name 77 | 78 | 79 | include 80 | #module_exports 81 | 82 | 83 | match 84 | [a-z]+ 85 | name 86 | invalid 87 | 88 | 89 | 90 | 91 | begin 92 | \b(class)\b 93 | beginCaptures 94 | 95 | 1 96 | 97 | name 98 | keyword.other.haskell 99 | 100 | 101 | end 102 | \b(where)\b|$ 103 | endCaptures 104 | 105 | 1 106 | 107 | name 108 | keyword.other.haskell 109 | 110 | 111 | name 112 | meta.declaration.class.haskell 113 | patterns 114 | 115 | 116 | match 117 | \b(Mon(ad|oid)|Functor|Applicative|(Folda|Traversa)ble|Eq|Ord|Read|Show|Num|(Frac|Ra)tional|Enum|Bounded|Real(Frac|Float)?|Integral|Floating)\b 118 | name 119 | support.class.prelude.haskell 120 | 121 | 122 | match 123 | [A-Z][A-Za-z_']* 124 | name 125 | entity.other.inherited-class.haskell 126 | 127 | 128 | match 129 | \b[a-z][a-zA-Z0-9_']*\b 130 | name 131 | variable.other.generic-type.haskell 132 | 133 | 134 | 135 | 136 | begin 137 | \b(instance)\b 138 | beginCaptures 139 | 140 | 1 141 | 142 | name 143 | keyword.other.haskell 144 | 145 | 146 | end 147 | \b(where)\b|$ 148 | endCaptures 149 | 150 | 1 151 | 152 | name 153 | keyword.other.haskell 154 | 155 | 156 | name 157 | meta.declaration.instance.haskell 158 | patterns 159 | 160 | 161 | include 162 | #type_signature 163 | 164 | 165 | 166 | 167 | begin 168 | ^(import)\b 169 | beginCaptures 170 | 171 | 1 172 | 173 | name 174 | keyword.other.haskell 175 | 176 | 177 | end 178 | ($|;) 179 | name 180 | meta.import.haskell 181 | patterns 182 | 183 | 184 | match 185 | \b(qualified|as|hiding)\b 186 | name 187 | keyword.other.haskell 188 | 189 | 190 | include 191 | #module_name 192 | 193 | 194 | include 195 | #module_exports 196 | 197 | 198 | include 199 | #comments 200 | 201 | 202 | 203 | 204 | begin 205 | (deriving)\s*\( 206 | beginCaptures 207 | 208 | 1 209 | 210 | name 211 | keyword.other.haskell 212 | 213 | 214 | end 215 | \) 216 | name 217 | meta.deriving.haskell 218 | patterns 219 | 220 | 221 | match 222 | \b[A-Z][a-zA-Z_']* 223 | name 224 | entity.other.inherited-class.haskell 225 | 226 | 227 | 228 | 229 | match 230 | \b(deriving|where|data|type|case|of|let|in|newtype|default)\b 231 | name 232 | keyword.other.haskell 233 | 234 | 235 | match 236 | \binfix[lr]?\b 237 | name 238 | keyword.operator.haskell 239 | 240 | 241 | match 242 | \b(do|if|then|else)\b 243 | name 244 | keyword.control.haskell 245 | 246 | 247 | comment 248 | Floats are always decimal 249 | match 250 | \b([0-9]+\.[0-9]+([eE][+-]?[0-9]+)?|[0-9]+[eE][+-]?[0-9]+)\b 251 | name 252 | constant.numeric.float.haskell 253 | 254 | 255 | match 256 | \b([0-9]+|0([xX][0-9a-fA-F]+|[oO][0-7]+))\b 257 | name 258 | constant.numeric.haskell 259 | 260 | 261 | captures 262 | 263 | 1 264 | 265 | name 266 | punctuation.definition.preprocessor.c 267 | 268 | 269 | comment 270 | In addition to Haskell's "native" syntax, GHC permits the C preprocessor to be run on a source file. 271 | match 272 | ^\s*(#)\s*\w+ 273 | name 274 | meta.preprocessor.c 275 | 276 | 277 | include 278 | #pragma 279 | 280 | 281 | begin 282 | " 283 | beginCaptures 284 | 285 | 0 286 | 287 | name 288 | punctuation.definition.string.begin.haskell 289 | 290 | 291 | end 292 | " 293 | endCaptures 294 | 295 | 0 296 | 297 | name 298 | punctuation.definition.string.end.haskell 299 | 300 | 301 | name 302 | string.quoted.double.haskell 303 | patterns 304 | 305 | 306 | 307 | begin 308 | \\\s*$ 309 | 310 | end 311 | \\ 312 | name 313 | constant.character.escape.multilinestring.haskell 314 | 315 | 316 | 317 | match 318 | \\(NUL|SOH|STX|ETX|EOT|ENQ|ACK|BEL|BS|HT|LF|VT|FF|CR|SO|SI|DLE|DC1|DC2|DC3|DC4|NAK|SYN|ETB|CAN|EM|SUB|ESC|FS|GS|RS|US|SP|DEL|[abfnrtv\\\"'\&]) 319 | name 320 | constant.character.escape.haskell 321 | 322 | 323 | match 324 | \\o[0-7]+|\\x[0-9A-Fa-f]+|\\[0-9]+ 325 | name 326 | constant.character.escape.octal.haskell 327 | 328 | 329 | match 330 | \^[A-Z@\[\]\\\^_] 331 | name 332 | constant.character.escape.control.haskell 333 | 334 | 335 | 336 | 337 | captures 338 | 339 | 1 340 | 341 | name 342 | punctuation.definition.string.begin.haskell 343 | 344 | 2 345 | 346 | name 347 | constant.character.escape.haskell 348 | 349 | 3 350 | 351 | name 352 | constant.character.escape.octal.haskell 353 | 354 | 4 355 | 356 | name 357 | constant.character.escape.hexadecimal.haskell 358 | 359 | 5 360 | 361 | name 362 | constant.character.escape.control.haskell 363 | 364 | 6 365 | 366 | name 367 | punctuation.definition.string.end.haskell 368 | 369 | 370 | match 371 | (?x) 372 | (') 373 | (?: 374 | [\ -\[\]-~] # Basic Char 375 | | (\\(?:NUL|SOH|STX|ETX|EOT|ENQ|ACK|BEL|BS|HT|LF|VT|FF|CR|SO|SI|DLE 376 | |DC1|DC2|DC3|DC4|NAK|SYN|ETB|CAN|EM|SUB|ESC|FS|GS|RS 377 | |US|SP|DEL|[abfnrtv\\\"'\&])) # Escapes 378 | | (\\o[0-7]+) # Octal Escapes 379 | | (\\x[0-9A-Fa-f]+) # Hexadecimal Escapes 380 | | (\^[A-Z@\[\]\\\^_]) # Control Chars 381 | ) 382 | (') 383 | 384 | name 385 | string.quoted.single.haskell 386 | 387 | 388 | begin 389 | ^\s*([a-z_][a-zA-Z0-9_']*|\([|!%$+\-.,=</>^&]+\))\s*(::|∷) 390 | beginCaptures 391 | 392 | 1 393 | 394 | name 395 | entity.name.function.haskell 396 | 397 | 2 398 | 399 | name 400 | keyword.other.double-colon.haskell 401 | 402 | 403 | end 404 | $\n? 405 | name 406 | meta.function.type-declaration.haskell 407 | patterns 408 | 409 | 410 | include 411 | #type_signature 412 | 413 | 414 | 415 | 416 | match 417 | ^(foreign)\s+(import|export)((\s+[\w_]+)*)(\s+\"(\\.|[^\"])*\")?\s*$ 418 | captures 419 | 420 | 1 421 | 422 | name 423 | keyword.other.haskell 424 | 425 | 2 426 | 427 | name 428 | keyword.other.haskell 429 | 430 | 3 431 | 432 | name 433 | keyword.other.haskell 434 | 435 | 5 436 | 437 | name 438 | string.quoted.double.haskell 439 | 440 | 441 | name 442 | meta.function.foreign-declaration.haskell 443 | 444 | 445 | begin 446 | ^(foreign)\s+(import|export)((\s+[\w_]+)*)(\s+\"(\\.|[^\"])*\")?\s+([a-z_][a-zA-Z0-9_']*|\([|!%$+\-.,=</>^&]+\))\s*((::)|∷) 447 | beginCaptures 448 | 449 | 1 450 | 451 | name 452 | keyword.other.haskell 453 | 454 | 2 455 | 456 | name 457 | keyword.other.haskell 458 | 459 | 3 460 | 461 | name 462 | keyword.other.haskell 463 | 464 | 5 465 | 466 | name 467 | string.quoted.double.haskell 468 | 469 | 7 470 | 471 | name 472 | entity.name.function.haskell 473 | 474 | 8 475 | 476 | name 477 | keyword.other.double-colon.haskell 478 | 479 | 480 | end 481 | $\n? 482 | name 483 | meta.function.type-declaration.haskell 484 | patterns 485 | 486 | 487 | include 488 | #type_signature 489 | 490 | 491 | 492 | 493 | begin 494 | (\[)([a-z][\w\d_]+)(\|) 495 | beginCaptures 496 | 497 | 1 498 | 499 | name 500 | keyword.operator.haskell 501 | 502 | 2 503 | 504 | name 505 | support.quasiquoter.template.haskell 506 | 507 | 3 508 | 509 | name 510 | keyword.operator.haskell 511 | 512 | 513 | end 514 | (\|\]) 515 | endCaptures 516 | 517 | 1 518 | 519 | name 520 | keyword.operator.haskell 521 | 522 | 523 | name 524 | string.quasiquoting.template.haskell 525 | 526 | 527 | match 528 | \b(Just|Nothing|Left|Right|True|False|LT|EQ|GT|\(\)|\[\])\b 529 | name 530 | support.constant.haskell 531 | 532 | 533 | match 534 | (?<!')\b[A-Z][A-Za-z_'0-9]* 535 | 536 | name 537 | constant.other.haskell 538 | 539 | 540 | include 541 | #comments 542 | 543 | 544 | match 545 | \b(abs|acos|acosh|all|and|any|appendFile|applyM|asTypeOf|asin|asinh|atan|atan2|atanh|break|catch|ceiling|compare|concat|concatMap|const|cos|cosh|curry|cycle|decodeFloat|div|divMod|drop|dropWhile|elem|encodeFloat|enumFrom|enumFromThen|enumFromThenTo|enumFromTo|error|even|exp|exponent|fail|filter|flip|floatDigits|floatRadix|floatRange|floor|fmap|foldMap|foldl|foldl1|foldr|foldr1|fromEnum|fromInteger|fromIntegral|fromRational|fst|gcd|getChar|getContents|getLine|head|id|init|interact|ioError|isDenormalized|isIEEE|isInfinite|isNaN|isNegativeZero|iterate|last|lcm|length|lex|lines|log|logBase|lookup|map|mapM|mapM_|max|maxBound|maximum|maybe|min|minBound|minimum|mod|negate|not|notElem|null|odd|or|otherwise|pi|pred|print|product|properFraction|putChar|putStr|putStrLn|quot|quotRem|read|readFile|readIO|readList|readLn|readParen|reads|readsPrec|realToFrac|recip|rem|repeat|replicate|return|reverse|round|scaleFloat|scanl|scanl1|scanr|scanr1|seq|sequence|sequence_|sequenceA|show|showChar|showList|showParen|showString|shows|showsPrec|significand|signum|sin|sinh|snd|span|splitAt|sqrt|subtract|succ|sum|tail|take|takeWhile|tan|tanh|toEnum|toInteger|toRational|traverse|truncate|uncurry|undefined|unlines|until|unwords|unzip|unzip3|userError|words|writeFile|zip|zip3|zipWith|zipWith3)\b 546 | name 547 | support.function.prelude.haskell 548 | 549 | 550 | include 551 | #infix_op 552 | 553 | 554 | comment 555 | In case this regex seems overly general, note that Haskell permits the definition of new operators which can be nearly any string of punctuation characters, such as $%^&*. 556 | match 557 | [|!%$?~+:\-.*=</>\\^&]+ 558 | name 559 | keyword.operator.haskell 560 | 561 | 562 | match 563 | , 564 | name 565 | punctuation.separator.comma.haskell 566 | 567 | 568 | repository 569 | 570 | block_comment 571 | 572 | applyEndPatternLast 573 | 1 574 | begin 575 | \{-(?!#) 576 | captures 577 | 578 | 0 579 | 580 | name 581 | punctuation.definition.comment.haskell 582 | 583 | 584 | end 585 | (?<!#)-\} 586 | 587 | name 588 | comment.block.haskell 589 | patterns 590 | 591 | 592 | include 593 | #block_comment 594 | 595 | 596 | 597 | comments 598 | 599 | patterns 600 | 601 | 602 | captures 603 | 604 | 1 605 | 606 | name 607 | punctuation.definition.comment.haskell 608 | 609 | 610 | match 611 | (---*(?!([!#\$%&\*\+\./<=>\?@\\\^\|\-~:]|[^[^\p{S}\p{P}]_"'\(\),;\[\]`\{}]))).*$\n? 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | name 631 | comment.line.double-dash.haskell 632 | 633 | 634 | include 635 | #block_comment 636 | 637 | 638 | 639 | infix_op 640 | 641 | match 642 | (\([|!%$+:\-.*=</>^&]+\)|\(,+\)) 643 | name 644 | entity.name.function.infix.haskell 645 | 646 | module_exports 647 | 648 | begin 649 | \( 650 | end 651 | \) 652 | name 653 | meta.declaration.exports.haskell 654 | patterns 655 | 656 | 657 | match 658 | \b[a-z][a-zA-Z_'0-9]* 659 | name 660 | entity.name.function.haskell 661 | 662 | 663 | match 664 | \b[A-Z][A-Za-z_'0-9]* 665 | name 666 | storage.type.haskell 667 | 668 | 669 | match 670 | , 671 | name 672 | punctuation.separator.comma.haskell 673 | 674 | 675 | include 676 | #infix_op 677 | 678 | 679 | comment 680 | So named because I don't know what to call this. 681 | match 682 | \(.*?\) 683 | name 684 | meta.other.unknown.haskell 685 | 686 | 687 | include 688 | #comments 689 | 690 | 691 | 692 | module_name 693 | 694 | match 695 | [A-Z][A-Za-z._']* 696 | name 697 | support.other.module.haskell 698 | 699 | pragma 700 | 701 | begin 702 | \{-# 703 | end 704 | #-\} 705 | name 706 | meta.preprocessor.haskell 707 | patterns 708 | 709 | 710 | match 711 | \b(LANGUAGE|UNPACK|INLINE)\b 712 | name 713 | keyword.other.preprocessor.haskell 714 | 715 | 716 | 717 | type_signature 718 | 719 | patterns 720 | 721 | 722 | captures 723 | 724 | 1 725 | 726 | name 727 | entity.other.inherited-class.haskell 728 | 729 | 2 730 | 731 | name 732 | variable.other.generic-type.haskell 733 | 734 | 3 735 | 736 | name 737 | keyword.other.big-arrow.haskell 738 | 739 | 740 | match 741 | \(\s*([A-Z][A-Za-z]*)\s+([a-z][A-Za-z_']*)\)\s*(=>) 742 | name 743 | meta.class-constraint.haskell 744 | 745 | 746 | include 747 | #pragma 748 | 749 | 750 | match 751 | (->|→) 752 | name 753 | keyword.other.arrow.haskell 754 | 755 | 756 | match 757 | (=>|⇒) 758 | name 759 | keyword.other.big-arrow.haskell 760 | 761 | 762 | match 763 | \b(Int(eger)?|Maybe|Either|Bool|Float|Double|Char|String|Ordering|ShowS|ReadS|FilePath|IO(Error)?)\b 764 | name 765 | support.type.prelude.haskell 766 | 767 | 768 | match 769 | \b[a-z][a-zA-Z0-9_']*\b 770 | name 771 | variable.other.generic-type.haskell 772 | 773 | 774 | match 775 | \b[A-Z][a-zA-Z0-9_']*\b 776 | name 777 | storage.type.haskell 778 | 779 | 780 | match 781 | (\(\)|★|\*) 782 | name 783 | support.constant.unit.haskell 784 | 785 | 786 | include 787 | #comments 788 | 789 | 790 | 791 | 792 | scopeName 793 | source.haskell 794 | uuid 795 | 5C034675-1F6D-497E-8073-369D37E2FD7D 796 | 797 | 798 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | 2 | [ ] Implement switching targets using Sublime's UI helpers. How do we get the list of possible targets? 3 | 4 | 5 | 6 | [ ] Implement tooltips for type and doc information. 7 | See here for Sublime Text API: 8 | https://www.sublimetext.com/forum/viewtopic.php?f=2&t=17583&start=40 9 | 10 | (maybe just use an "info" panel for this?) 11 | 12 | See here for ide-backend-client API: 13 | https://github.com/chrisdone/ide-backend-client/tree/master/ide-backend-client 14 | 15 | 16 | [ ] Figure out autocompletion for qualified names, e.g. Map.insert 17 | It may be necessary to remove . from the "word_separators" sublime preference to get sublime to report the full name to the autocompletion plugin, 18 | i.e. change it to: 19 | "word_separators": "/\\()\"'-:,;<>~!@#$%^&*|+=[]{}`~?", 20 | (note that in the default there are two occurrences of . - be sure to remove them both) 21 | This wasn't quite enough, so test stack-ide/ide-backend to see if it handles qualified autocompletion correctly. 22 | 23 | 24 | [ ] Perhaps change references to GHC.Base, GHC.List, etc., in autocomplete into just "Prelude"? I find them noisy. 25 | 26 | 27 | DONE: 28 | 29 | [x] End sessions correctly on Window close event (worked around no window close event with a watchdog) 30 | 31 | [x] Completions are asynchronous, so we only see them if we type e.g. ff or on the second letter. 32 | See if we can re-trigger the completion list once the completions arrive. 33 | 34 | 35 | [x] Implement getExpTypes to just follow the cursor around and place the type in the status bar? 36 | To complement the larger getSpanInfo above. 37 | Need to ensure it's persistent enough and maybe log it to a buffer too for copy-pasteability 38 | (Oh, we can probably use the status flag API for the persistence part) 39 | 40 | [x] Use `stack ide load-targets` asynchronously at startup (and maybe with a key command) and have it pass its results into update_session_includes at startup. We can keep the current "on save, add to update_session_includes", but we should keep a persistent list of them so we always pass the full list. We may need a "clear" command with this approach, in case you save two modules containing a main function, for example (is there a better way?). 41 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukexi/stack-ide-sublime/89c7a1b342183a107ec1e188902e828eb87c41e7/__init__.py -------------------------------------------------------------------------------- /application_commands.py: -------------------------------------------------------------------------------- 1 | import sublime_plugin 2 | import sys, os 3 | sys.path.append(os.path.dirname(os.path.realpath(__file__))) 4 | 5 | from stack_ide_manager import StackIDEManager 6 | 7 | 8 | class RestartStackIde(sublime_plugin.ApplicationCommand): 9 | """ 10 | Restarts the StackIDE plugin. 11 | Useful for forcing StackIDE to pick up project changes, until we implement it properly. 12 | Accessible via the Command Palette (Cmd/Ctrl-Shift-p) 13 | as "SublimeStackIDE: Restart" 14 | """ 15 | def run(self): 16 | StackIDEManager.reset() 17 | -------------------------------------------------------------------------------- /event_listeners.py: -------------------------------------------------------------------------------- 1 | try: 2 | import sublime_plugin, sublime 3 | except ImportError: 4 | from test.stubs import sublime, sublime_plugin 5 | 6 | import sys, os 7 | sys.path.append(os.path.dirname(os.path.realpath(__file__))) 8 | 9 | from utility import is_haskell_view, relative_view_file_name, span_from_view_selection 10 | from req import Req 11 | from win import Win 12 | from stack_ide_manager import StackIDEManager, send_request 13 | from response import parse_autocompletions 14 | 15 | class StackIDESaveListener(sublime_plugin.EventListener): 16 | """ 17 | Ask stack-ide to recompile the saved source file, 18 | then request a report of source errors. 19 | """ 20 | def on_post_save(self, view): 21 | 22 | if not is_haskell_view(view): 23 | return 24 | 25 | if not StackIDEManager.is_running(view.window()): 26 | return 27 | 28 | StackIDEManager.for_window(view.window()).update_files([relative_view_file_name(view)]) 29 | 30 | class StackIDETypeAtCursorHandler(sublime_plugin.EventListener): 31 | """ 32 | Ask stack-ide for the type at the cursor each 33 | time it changes position. 34 | """ 35 | def on_selection_modified(self, view): 36 | 37 | if not is_haskell_view(view): 38 | return 39 | 40 | window = view.window() 41 | if not StackIDEManager.is_running(window): 42 | return 43 | 44 | # Only try to get types for views into files 45 | # (rather than e.g. the find field or the console pane) 46 | if view.file_name(): 47 | # Uncomment to see the scope at the cursor: 48 | # Log.debug(view.scope_name(view.sel()[0].begin())) 49 | request = Req.get_exp_types(span_from_view_selection(view)) 50 | send_request(window, request, Win(window).highlight_type) 51 | 52 | 53 | class StackIDEAutocompleteHandler(sublime_plugin.EventListener): 54 | """ 55 | Dispatches autocompletion requests to stack-ide. 56 | """ 57 | def __init__(self): 58 | super(StackIDEAutocompleteHandler, self).__init__() 59 | self.returned_completions = [] 60 | self.view = None 61 | self.refreshing = False 62 | 63 | def on_query_completions(self, view, prefix, locations): 64 | 65 | if not is_haskell_view(view): 66 | return 67 | 68 | window = view.window() 69 | if not StackIDEManager.is_running(window): 70 | return 71 | # Check if this completion query is due to our refreshing the completions list 72 | # after receiving a response from stack-ide, and if so, don't send 73 | # another request for completions. 74 | if not self.refreshing: 75 | self.view = view 76 | request = Req.get_autocompletion(filepath=relative_view_file_name(view),prefix=prefix) 77 | send_request(window, request, self._handle_response) 78 | 79 | # Clear the flag to allow future completion queries 80 | self.refreshing = False 81 | return list(self.format_completion(*completion) for completion in self.returned_completions) 82 | 83 | 84 | def format_completion(self, prop, scope): 85 | return ["{}\t{}\t{}".format(prop.name, 86 | prop.type or '', 87 | scope.importedFrom.module if scope else ''), 88 | prop.name] 89 | 90 | def _handle_response(self, response): 91 | self.returned_completions = list(parse_autocompletions(response)) 92 | self.view.run_command('hide_auto_complete') 93 | sublime.set_timeout(self.run_auto_complete, 0) 94 | 95 | 96 | def run_auto_complete(self): 97 | self.refreshing = True 98 | self.view.run_command("auto_complete", { 99 | 'disable_auto_insert': True, 100 | # 'api_completions_only': True, 101 | 'next_completion_if_showing': False, 102 | # 'auto_complete_commit_on_tab': True, 103 | }) 104 | -------------------------------------------------------------------------------- /log.py: -------------------------------------------------------------------------------- 1 | try: 2 | import sublime 3 | except ImportError: 4 | from test.stubs import sublime 5 | 6 | class Log: 7 | """ 8 | Logging facilities 9 | """ 10 | 11 | verbosity = None 12 | 13 | VERB_NONE = 0 14 | VERB_ERROR = 1 15 | VERB_WARNING = 2 16 | VERB_NORMAL = 3 17 | VERB_DEBUG = 4 18 | 19 | @classmethod 20 | def reset(cls): 21 | Log.verbosity = None 22 | 23 | @classmethod 24 | def error(cls,*msg): 25 | Log._record(Log.VERB_ERROR, *msg) 26 | 27 | @classmethod 28 | def warning(cls,*msg): 29 | Log._record(Log.VERB_WARNING, *msg) 30 | 31 | @classmethod 32 | def normal(cls,*msg): 33 | Log._record(Log.VERB_NORMAL, *msg) 34 | 35 | @classmethod 36 | def debug(cls,*msg): 37 | Log._record(Log.VERB_DEBUG, *msg) 38 | 39 | @classmethod 40 | def _record(cls, verb, *msg): 41 | if not Log.verbosity: 42 | Log._set_verbosity("none") 43 | 44 | if verb <= Log.verbosity: 45 | for line in ''.join(map(lambda x: str(x), msg)).split('\n'): 46 | print('[SublimeStackIDE]['+cls._show_verbosity(verb)+']:',*msg) 47 | 48 | if verb == Log.VERB_ERROR: 49 | sublime.status_message('There were errors, check the console log') 50 | elif verb == Log.VERB_WARNING: 51 | sublime.status_message('There were warnings, check the console log') 52 | 53 | @classmethod 54 | def _set_verbosity(cls, input): 55 | 56 | verb = input.lower() 57 | 58 | if verb == "none": 59 | Log.verbosity = Log.VERB_NONE 60 | elif verb == "error": 61 | Log.verbosity = Log.VERB_ERROR 62 | elif verb == "warning": 63 | Log.verbosity = Log.VERB_WARNING 64 | elif verb == "normal": 65 | Log.verbosity = Log.VERB_NORMAL 66 | elif verb == "debug": 67 | Log.verbosity = Log.VERB_DEBUG 68 | else: 69 | Log.verbosity = Log.VERB_WARNING 70 | Log.warning("Invalid verbosity: '" + str(verb) + "'") 71 | 72 | @classmethod 73 | def _show_verbosity(cls,verb): 74 | return ["?!","ERROR","WARN","NORM","DEBUG"][verb] 75 | -------------------------------------------------------------------------------- /req.py: -------------------------------------------------------------------------------- 1 | class Req: 2 | @staticmethod 3 | def update_session_includes(filepaths): 4 | return { 5 | "tag":"RequestUpdateSession", 6 | "contents": 7 | [ { "tag": "RequestUpdateTargets", 8 | "contents": {"tag": "TargetsInclude", "contents": filepaths } 9 | } 10 | ] 11 | } 12 | 13 | @staticmethod 14 | def update_session(): 15 | return { "tag":"RequestUpdateSession", "contents": []} 16 | 17 | @staticmethod 18 | def get_source_errors(): 19 | return {"tag": "RequestGetSourceErrors", "contents":[]} 20 | 21 | @staticmethod 22 | def get_exp_types(exp_span): 23 | return { "tag": "RequestGetExpTypes", "contents": exp_span} 24 | 25 | @staticmethod 26 | def get_exp_info(exp_span): 27 | return { "tag": "RequestGetSpanInfo", "contents": exp_span} 28 | 29 | @staticmethod 30 | def get_shutdown(): 31 | return {"tag":"RequestShutdownSession", "contents":[]} 32 | 33 | @staticmethod 34 | def get_autocompletion(filepath,prefix): 35 | return { 36 | "tag":"RequestGetAutocompletion", 37 | "contents": [ 38 | filepath, 39 | prefix 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /response.py: -------------------------------------------------------------------------------- 1 | ############################################# 2 | # PARSING 3 | # 4 | # see: https://github.com/commercialhaskell/stack-ide/blob/master/stack-ide-api/src/Stack/Ide/JsonAPI.hs 5 | # and: https://github.com/fpco/ide-backend/blob/master/ide-backend-common/IdeSession/Types/Public.hs 6 | # Types of responses: 7 | # ResponseGetSourceErrors [SourceError] 8 | # ResponseGetLoadedModules [ModuleName] 9 | # ResponseGetSpanInfo [ResponseSpanInfo] 10 | # ResponseGetExpTypes [ResponseExpType] 11 | # ResponseGetAnnExpTypes [ResponseAnnExpType] 12 | # ResponseGetAutocompletion [IdInfo] 13 | # ResponseUpdateSession 14 | # ResponseLog 15 | 16 | def parse_autocompletions(contents): 17 | """ 18 | Converts ResponseGetAutoCompletion content into [(IdProp, IdScope)] 19 | """ 20 | return ((parse_idprop(item.get('idProp')), 21 | parse_idscope(item.get('idScope'))) for item in contents) 22 | 23 | 24 | def parse_update_session(contents): 25 | """ 26 | Converts a ResponseUpdateSession message to a single status string 27 | """ 28 | tag = contents.get('tag') 29 | if tag == "UpdateStatusProgress": 30 | progress = contents.get('contents') 31 | return str(progress.get("progressParsedMsg")) 32 | elif tag == "UpdateStatusDone": 33 | return " " 34 | elif tag == "UpdateStatusRequiredRestart": 35 | return "Starting session..." 36 | 37 | 38 | def parse_source_errors(contents): 39 | """ 40 | Converts ResponseGetSourceErrors content into an array of SourceError objects 41 | """ 42 | return (SourceError(item.get('errorKind'), 43 | item.get('errorMsg'), 44 | parse_either_span(item.get('errorSpan'))) for item in contents) 45 | 46 | 47 | def parse_exp_types(contents): 48 | """ 49 | Converts ResponseGetExpTypes contents into an array of pairs containing 50 | Text and SourceSpan 51 | Also see: type_info_for_sel (replace) 52 | """ 53 | return ((item[0], parse_source_span(item[1])) for item in contents) 54 | 55 | 56 | def parse_span_info_response(contents): 57 | """ 58 | Converts ResponseGetSpanInfo contents into an array of pairs of SpanInfo and SourceSpan objects 59 | ResponseGetSpanInfo's contents are an array of SpanInfo and SourceSpan pairs 60 | """ 61 | return ((parse_span_info(responseSpanInfo[0]), 62 | parse_source_span(responseSpanInfo[1])) for responseSpanInfo in contents) 63 | 64 | 65 | def parse_span_info(json): 66 | """ 67 | Converts SpanInfo contents into a pair of IdProp and IdScope objects 68 | 69 | :param dict json: responds to a Span type from Stack IDE 70 | 71 | SpanInfo is either 'tag' SpanId or 'tag' SpanQQ, with an nested under as contents IdInfo 72 | TODO: deal with SpanQQ here 73 | """ 74 | contents = json.get('contents') 75 | return (parse_idprop(contents.get('idProp')), 76 | parse_idscope(contents.get('idScope'))) 77 | 78 | 79 | def parse_idprop(values): 80 | """ 81 | Converts idProp content into an IdProp object. 82 | """ 83 | return IdProp(values.get('idDefinedIn').get('moduleName'), 84 | values.get('idDefinedIn').get('modulePackage').get('packageName'), 85 | values.get('idType'), 86 | values.get('idName'), 87 | parse_either_span(values.get('idDefSpan'))) 88 | 89 | 90 | def parse_idscope(values): 91 | """ 92 | Converts idScope content into an IdScope object (containing only an IdImportedFrom) 93 | """ 94 | importedFrom = values.get('idImportedFrom') 95 | return IdScope(IdImportedFrom(importedFrom.get('moduleName'), 96 | importedFrom.get('modulePackage').get('packageName'))) if importedFrom else None 97 | 98 | 99 | def parse_either_span(json): 100 | """ 101 | Checks EitherSpan content and returns a SourceSpan if possible. 102 | """ 103 | if json.get('tag') == 'ProperSpan': 104 | return parse_source_span(json.get('contents')) 105 | else: 106 | return None 107 | 108 | 109 | def parse_source_span(json): 110 | """ 111 | Converts json into a SourceSpan 112 | """ 113 | paths = ['spanFilePath', 'spanFromLine', 'spanFromColumn', 'spanToLine', 'spanToColumn'] 114 | fields = get_paths(paths, json) 115 | return SourceSpan(*fields) if fields else None 116 | 117 | 118 | def get_paths(paths, values): 119 | """ 120 | Converts a list of keypaths into an array of values from a dict 121 | """ 122 | return list(values.get(path) for path in paths) 123 | 124 | 125 | class SourceError(): 126 | 127 | def __init__(self, kind, message, span): 128 | self.kind = kind 129 | self.msg = message 130 | self.span = span 131 | 132 | def __repr__(self): 133 | if self.span: 134 | return "{file}:{from_line}:{from_column}: {kind}:\n{msg}".format( 135 | file=self.span.filePath, 136 | from_line=self.span.fromLine, 137 | from_column=self.span.fromColumn, 138 | kind=self.kind, 139 | msg=self.msg) 140 | else: 141 | return self.msg 142 | 143 | 144 | class SourceSpan(): 145 | 146 | def __init__(self, filePath, fromLine, fromColumn, toLine, toColumn): 147 | self.filePath = filePath 148 | self.fromLine = fromLine 149 | self.fromColumn = fromColumn 150 | self.toLine = toLine 151 | self.toColumn = toColumn 152 | 153 | 154 | class IdScope(): 155 | 156 | def __init__(self, importedFrom): 157 | self.importedFrom = importedFrom 158 | 159 | 160 | class IdImportedFrom(): 161 | 162 | def __init__(self, module, package): 163 | self.module = module 164 | self.package = package 165 | 166 | 167 | class IdProp(): 168 | 169 | def __init__(self, package, module, type, name, defSpan): 170 | self.package = package 171 | self.module = module 172 | self.type = type 173 | self.name = name 174 | self.defSpan = defSpan 175 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | class Settings: 2 | 3 | def __init__(self, verbosity, add_to_PATH, show_popup, hoogle_url=None): 4 | self.verbosity = verbosity 5 | self.add_to_PATH = add_to_PATH 6 | self.show_popup = show_popup 7 | self.hoogle_url = hoogle_url 8 | -------------------------------------------------------------------------------- /stack_ide.py: -------------------------------------------------------------------------------- 1 | try: 2 | import sublime 3 | except ImportError: 4 | from test.stubs import sublime 5 | 6 | import subprocess, os 7 | import sys 8 | import threading 9 | import json 10 | import uuid 11 | 12 | sys.path.append(os.path.dirname(os.path.realpath(__file__))) 13 | 14 | from utility import first_folder, complain 15 | from req import Req 16 | from log import Log 17 | from win import Win 18 | import response as res 19 | 20 | # Make sure Popen hides the console on Windows. 21 | # We don't need this on other platforms 22 | # (and it would cause an error) 23 | CREATE_NO_WINDOW = 0 24 | if os.name == 'nt': 25 | CREATE_NO_WINDOW = 0x08000000 26 | 27 | class StackIDE: 28 | 29 | 30 | def __init__(self, window, settings, backend=None): 31 | self.window = window 32 | 33 | self.conts = {} # Map from uuid to response handler 34 | self.is_alive = True 35 | self.is_active = False 36 | self.process = None 37 | self.project_path = first_folder(window) 38 | (project_in, project_name) = os.path.split(self.project_path) 39 | self.project_name = project_name 40 | 41 | reset_env(settings.add_to_PATH) 42 | 43 | if backend is None: 44 | self._backend = stack_ide_start(self.project_path, self.project_name, self.handle_response) 45 | else: # for testing 46 | self._backend = backend 47 | self._backend.handler = self.handle_response 48 | 49 | self.is_active = True 50 | self.include_targets = set() 51 | 52 | # TODO: could check packages here to fix the 'project_dir must equal packagename issue' 53 | 54 | sublime.set_timeout_async(self.load_initial_targets, 0) 55 | 56 | 57 | def send_request(self, request, response_handler = None): 58 | """ 59 | Associates requests with handlers and passes them on to the process. 60 | """ 61 | if self._backend: 62 | if response_handler is not None: 63 | seq_id = str(uuid.uuid4()) 64 | self.conts[seq_id] = response_handler 65 | request = request.copy() 66 | request['seq'] = seq_id 67 | 68 | self._backend.send_request(request) 69 | else: 70 | Log.error("Couldn't send request, no process!", request) 71 | 72 | 73 | def load_initial_targets(self): 74 | """ 75 | Get the initial list of files to check 76 | """ 77 | initial_targets = stack_ide_loadtargets(self.project_path, self.project_name) 78 | sublime.set_timeout(lambda: self.update_files(initial_targets), 0) 79 | 80 | 81 | def update_new_include_targets(self, filepaths): 82 | for filepath in filepaths: 83 | self.include_targets.add(filepath) 84 | return list(self.include_targets) 85 | 86 | def update_files(self, filenames): 87 | new_include_targets = self.update_new_include_targets(filenames) 88 | self.send_request(Req.update_session_includes(new_include_targets)) 89 | self.send_request(Req.get_source_errors(), Win(self.window).handle_source_errors) 90 | 91 | def end(self): 92 | """ 93 | Ask stack-ide to shut down. 94 | """ 95 | Win(self.window).hide_error_panel() 96 | self.send_request(Req.get_shutdown()) 97 | self.die() 98 | 99 | def die(self): 100 | """ 101 | Mark the instance as no longer alive 102 | """ 103 | self.is_alive = False 104 | self.is_active = False 105 | 106 | def handle_response(self, data): 107 | """ 108 | Handles JSON responses from the backend 109 | """ 110 | Log.debug("Got response: ", data) 111 | 112 | tag = data.get("tag") 113 | contents = data.get("contents") 114 | seq_id = data.get("seq") 115 | 116 | if seq_id is not None: 117 | self._send_to_handler(contents, seq_id) 118 | 119 | elif tag == "ResponseWelcome": 120 | self._handle_welcome(contents) 121 | 122 | elif tag == "ResponseUpdateSession": 123 | self._handle_update_session(contents) 124 | 125 | elif tag == "ResponseShutdownSession": 126 | Log.debug("Stack-ide process has shut down") 127 | 128 | elif tag == "ResponseLog": 129 | Log.debug(contents.rstrip()) 130 | 131 | else: 132 | Log.normal("Unhandled response: ", data) 133 | 134 | def _send_to_handler(self, contents, seq_id): 135 | """ 136 | Looks up a previously registered handler for the incoming response 137 | """ 138 | handler = self.conts.get(seq_id) 139 | del self.conts[seq_id] 140 | if handler is not None: 141 | if contents is not None: 142 | sublime.set_timeout(lambda:handler(contents), 0) 143 | else: 144 | Log.warning("Handler not found for seq", seq_id) 145 | 146 | 147 | def _handle_welcome(self, welcome): 148 | """ 149 | Identifies if we support the current version of the stack ide api 150 | """ 151 | expected_version = (0,1,1) 152 | version_got = tuple(welcome) if type(welcome) is list else welcome 153 | if expected_version > version_got: 154 | Log.error("Old stack-ide protocol:", version_got, '\n', 'Want version:', expected_version) 155 | complain("wrong-stack-ide-version", 156 | "Please upgrade stack-ide to a newer version.") 157 | elif expected_version < version_got: 158 | Log.warning("stack-ide protocol may have changed:", version_got) 159 | else: 160 | Log.debug("stack-ide protocol version:", version_got) 161 | 162 | 163 | def _handle_update_session(self, update_session): 164 | """ 165 | Show a status message for session progress updates. 166 | """ 167 | msg = res.parse_update_session(update_session) 168 | if msg: 169 | sublime.status_message(msg) 170 | 171 | 172 | def __del__(self): 173 | if self.process: 174 | try: 175 | self.process.terminate() 176 | except ProcessLookupError: 177 | # it was already done... 178 | pass 179 | finally: 180 | self.process = None 181 | 182 | env = {} 183 | 184 | def reset_env(add_to_PATH): 185 | global env 186 | env = os.environ.copy() 187 | if len(add_to_PATH) > 0: 188 | env["PATH"] = os.pathsep.join(add_to_PATH + [env.get("PATH","")]) 189 | 190 | 191 | def stack_ide_packages(project_path): 192 | proc = subprocess.Popen(["stack", "ide", "packages"], 193 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 194 | cwd=project_path, env=env, 195 | universal_newlines=True, 196 | creationflags=CREATE_NO_WINDOW) 197 | outs, errs = proc.communicate() 198 | return outs.splitlines() 199 | 200 | 201 | def stack_ide_loadtargets(project_path, package): 202 | 203 | Log.debug("Requesting load targets for ", package) 204 | proc = subprocess.Popen(["stack", "ide", "load-targets", package], 205 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 206 | cwd=project_path, env=env, 207 | universal_newlines=True, 208 | creationflags=CREATE_NO_WINDOW) 209 | outs, errs = proc.communicate() 210 | # TODO: check response! 211 | return outs.splitlines() 212 | 213 | 214 | def stack_ide_start(project_path, package, response_handler): 215 | """ 216 | Start up a stack-ide subprocess for the window, and a thread to consume its stdout. 217 | """ 218 | 219 | Log.debug("Calling stack ide start with PATH:", env['PATH'] if env else os.environ['PATH']) 220 | 221 | process = subprocess.Popen(["stack", "ide", "start", package], 222 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 223 | cwd=project_path, env=env, 224 | creationflags=CREATE_NO_WINDOW 225 | ) 226 | 227 | return JsonProcessBackend(process, response_handler) 228 | 229 | 230 | class JsonProcessBackend: 231 | """ 232 | Handles process communication with JSON. 233 | """ 234 | def __init__(self, process, response_handler): 235 | self._process = process 236 | self._response_handler = response_handler 237 | self.stdoutThread = threading.Thread(target=self.read_stdout) 238 | self.stdoutThread.start() 239 | self.stderrThread = threading.Thread(target=self.read_stderr) 240 | self.stderrThread.start() 241 | 242 | def send_request(self, request): 243 | 244 | try: 245 | Log.debug("Sending request: ", request) 246 | encodedString = json.JSONEncoder().encode(request) + "\n" 247 | self._process.stdin.write(bytes(encodedString, 'UTF-8')) 248 | self._process.stdin.flush() 249 | except BrokenPipeError as e: 250 | Log.error("stack-ide unexpectedly died:",e) 251 | 252 | # self.die() 253 | # Ideally we would like to die(), so that, if the error is transient, 254 | # we attempt to reconnect on the next check_windows() call. The problem 255 | # is that the stack-ide (ide-backend, actually) is not cleaning up those 256 | # session.* directories and they would keep accumulating, one per second! 257 | # So instead we do: 258 | self.is_active = False 259 | 260 | 261 | def read_stderr(self): 262 | """ 263 | Reads any errors from the stack-ide process. 264 | """ 265 | while self._process.poll() is None: 266 | 267 | try: 268 | error = self._process.stderr.readline().decode('UTF-8') 269 | if len(error) > 0: 270 | Log.warning("Stack-IDE error: ", error) 271 | except: 272 | Log.error("Stack-IDE stderr process ending due to exception: ", sys.exc_info()) 273 | return 274 | 275 | Log.debug("Stack-IDE stderr process ended.") 276 | 277 | def read_stdout(self): 278 | """ 279 | Reads JSON responses from stack-ide and dispatch them to 280 | various main thread handlers. 281 | """ 282 | while self._process.poll() is None: 283 | try: 284 | raw = self._process.stdout.readline().decode('UTF-8') 285 | if not raw: 286 | return 287 | 288 | data = None 289 | try: 290 | data = json.loads(raw) 291 | except: 292 | Log.debug("Got a non-JSON response: ", raw) 293 | continue 294 | 295 | #todo: try catch ? 296 | self._response_handler(data) 297 | 298 | except: 299 | Log.warning("Stack-IDE stdout process ending due to exception: ", sys.exc_info()) 300 | self._process.terminate() 301 | self._process = None 302 | return 303 | 304 | Log.info("Stack-IDE stdout process ended.") 305 | 306 | -------------------------------------------------------------------------------- /stack_ide_manager.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | import sys 3 | import os 4 | 5 | sys.path.append(os.path.dirname(os.path.realpath(__file__))) 6 | 7 | from stack_ide import StackIDE 8 | from log import Log 9 | from utility import first_folder,expected_cabalfile,has_cabal_file, is_stack_project, complain, reset_complaints 10 | try: 11 | import sublime 12 | except ImportError: 13 | from test.stubs import sublime 14 | 15 | def send_request(window, request, on_response = None): 16 | """ 17 | Sends the given request to the (view's) window's stack-ide instance, 18 | optionally handling its response 19 | """ 20 | if StackIDEManager.is_running(window): 21 | StackIDEManager.for_window(window).send_request(request, on_response) 22 | 23 | def configure_instance(window, settings): 24 | 25 | folder = first_folder(window) 26 | 27 | if not folder: 28 | msg = "No folder to monitor for window " + str(window.id()) 29 | Log.normal("Window {}: {}".format(str(window.id()), msg)) 30 | instance = NoStackIDE(msg) 31 | 32 | elif not has_cabal_file(folder): 33 | msg = "No cabal file found in " + folder 34 | Log.normal("Window {}: {}".format(str(window.id()), msg)) 35 | instance = NoStackIDE(msg) 36 | 37 | elif not os.path.isfile(expected_cabalfile(folder)): 38 | msg = "Expected cabal file " + expected_cabalfile(folder) + " not found" 39 | Log.normal("Window {}: {}".format(str(window.id()), msg)) 40 | instance = NoStackIDE(msg) 41 | 42 | elif not is_stack_project(folder): 43 | msg = "No stack.yaml in path " + folder 44 | Log.warning("Window {}: {}".format(str(window.id()), msg)) 45 | instance = NoStackIDE(msg) 46 | 47 | # TODO: We should also support single files, which should get their own StackIDE instance 48 | # which would then be per-view. Have a registry per-view that we check, then check the window. 49 | 50 | else: 51 | try: 52 | # If everything looks OK, launch a StackIDE instance 53 | Log.normal("Initializing window", window.id()) 54 | instance = StackIDE(window, settings) 55 | except FileNotFoundError as e: 56 | instance = NoStackIDE("instance init failed -- stack not found") 57 | Log.error(e) 58 | complain('stack-not-found', 59 | "Could not find program 'stack'!\n\n" 60 | "Make sure that 'stack' and 'stack-ide' are both installed. " 61 | "If they are not on the system path, edit the 'add_to_PATH' " 62 | "setting in SublimeStackIDE preferences." ) 63 | except Exception: 64 | instance = NoStackIDE("instance init failed -- unknown error") 65 | Log.error("Failed to initialize window " + str(window.id()) + ":") 66 | Log.error(traceback.format_exc()) 67 | 68 | 69 | return instance 70 | 71 | 72 | class StackIDEManager: 73 | ide_backend_instances = {} 74 | settings = None 75 | 76 | @classmethod 77 | def getinstances(cls): 78 | return cls.ide_backend_instances 79 | 80 | @classmethod 81 | def check_windows(cls): 82 | """ 83 | Compares the current windows with the list of instances: 84 | - new windows are assigned a process of stack-ide each 85 | - stale processes are stopped 86 | 87 | NB. This is the only method that updates ide_backend_instances, 88 | so as long as it is not called concurrently, there will be no 89 | race conditions... 90 | """ 91 | current_windows = {w.id(): w for w in sublime.windows()} 92 | updated_instances = {} 93 | 94 | # Kill stale instances, keep live ones 95 | for win_id,instance in StackIDEManager.ide_backend_instances.items(): 96 | if win_id not in current_windows: 97 | # This is a window that is now closed, we may need to kill its process 98 | if instance.is_active: 99 | Log.normal("Stopping stale process for window", win_id) 100 | instance.end() 101 | else: 102 | # This window is still active. There are three possibilities: 103 | # 1) it has an alive and active instance. 104 | # 2) it has an alive but inactive instance (one that failed to init, etc) 105 | # 3) it has a dead instance, i.e., one that was killed. 106 | # 107 | # A window with a dead instances is treated like a new one, so we will 108 | # try to launch a new instance for it 109 | if instance.is_alive: 110 | del current_windows[win_id] 111 | updated_instances[win_id] = instance 112 | 113 | StackIDEManager.ide_backend_instances = updated_instances 114 | # Thw windows remaining in current_windows are new, so they have no instance. 115 | # We try to create one for them 116 | for window in current_windows.values(): 117 | StackIDEManager.ide_backend_instances[window.id()] = configure_instance(window, cls.settings) 118 | 119 | 120 | @classmethod 121 | def is_running(cls, window): 122 | if not window: 123 | return False 124 | return StackIDEManager.for_window(window) is not None 125 | 126 | 127 | @classmethod 128 | def for_window(cls, window): 129 | instance = StackIDEManager.ide_backend_instances.get(window.id()) 130 | if instance and not instance.is_active: 131 | instance = None 132 | 133 | return instance 134 | 135 | @classmethod 136 | def kill_all(cls): 137 | # Log.normal("Killing all stack-ide-sublime instances:", {k:str(v) for k, v in StackIDEManager.ide_backend_instances.items()}) 138 | for instance in StackIDEManager.ide_backend_instances.values(): 139 | instance.end() 140 | 141 | @classmethod 142 | def reset(cls): 143 | """ 144 | Kill all instances, and forget about previous notifications. 145 | """ 146 | Log.normal("Resetting StackIDE") 147 | StackIDEManager.kill_all() 148 | reset_complaints() 149 | 150 | @classmethod 151 | def configure(cls, settings): 152 | cls.settings = settings 153 | 154 | 155 | class NoStackIDE: 156 | """ 157 | Objects of this class are used for windows that don't have an associated stack-ide process 158 | (e.g., because initialization failed or they are not being monitored) 159 | """ 160 | 161 | def __init__(self, reason): 162 | self.is_alive = True 163 | self.is_active = False 164 | self.reason = reason 165 | 166 | def end(self): 167 | self.is_alive = False 168 | 169 | def __str__(self): 170 | return 'NoStackIDE(' + self.reason + ')' 171 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukexi/stack-ide-sublime/89c7a1b342183a107ec1e188902e828eb87c41e7/test/__init__.py -------------------------------------------------------------------------------- /test/data.py: -------------------------------------------------------------------------------- 1 | from settings import Settings 2 | 3 | type_info = "FilePath -> IO String" 4 | span = { 5 | "spanFromLine": 1, 6 | "spanFromColumn": 1, 7 | "spanToLine": 1, 8 | "spanToColumn": 5 9 | } 10 | exp_types_response = {"tag": "", "contents": [[type_info, span]]} 11 | someFunc_span_info = {'contents': [[{'contents': {'idProp': {'idDefinedIn': {'moduleName': 'Lib', 'modulePackage': {'packageVersion': None, 'packageName': 'main', 'packageKey': 'main'}}, 'idSpace': 'VarName', 'idType': 'IO ()', 'idDefSpan': {'contents': {'spanFromLine': 9, 'spanFromColumn': 1, 'spanToColumn': 9, 'spanFilePath': 'src/Lib.hs', 'spanToLine': 9}, 'tag': 'ProperSpan'}, 'idName': 'someFunc', 'idHomeModule': None}, 'idScope': {'idImportQual': '', 'idImportedFrom': {'moduleName': 'Lib', 'modulePackage': {'packageVersion': None, 'packageName': 'main', 'packageKey': 'main'}}, 'idImportSpan': {'contents': {'spanFromLine': 3, 'spanFromColumn': 1, 'spanToColumn': 11, 'spanFilePath': 'app/Main.hs', 'spanToLine': 3}, 'tag': 'ProperSpan'}, 'tag': 'Imported'}}, 'tag': 'SpanId'}, {'spanFromLine': 7, 'spanFromColumn': 27, 'spanToColumn': 35, 'spanFilePath': 'app/Main.hs', 'spanToLine': 7}]], 'seq': '724752c9-a7bf-4658-834a-3ff7df64e7e5', 'tag': 'ResponseGetSpanInfo'} 12 | putStrLn_span_info = {'contents': [[{'contents': {'idProp': {'idDefinedIn': {'moduleName': 'System.IO', 'modulePackage': {'packageVersion': '4.8.1.0', 'packageName': 'base', 'packageKey': 'base'}}, 'idSpace': 'VarName', 'idType': 'String -> IO ()', 'idDefSpan': {'contents': '', 'tag': 'TextSpan'}, 'idName': 'putStrLn', 'idHomeModule': {'moduleName': 'System.IO', 'modulePackage': {'packageVersion': '4.8.1.0', 'packageName': 'base', 'packageKey': 'base'}}}, 'idScope': {'idImportQual': '', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageVersion': '4.8.1.0', 'packageName': 'base', 'packageKey': 'base'}}, 'idImportSpan': {'contents': {'spanFromLine': 1, 'spanFromColumn': 8, 'spanToColumn': 12, 'spanFilePath': 'app/Main.hs', 'spanToLine': 1}, 'tag': 'ProperSpan'}, 'tag': 'Imported'}}, 'tag': 'SpanId'}, {'spanFromLine': 7, 'spanFromColumn': 41, 'spanToColumn': 49, 'spanFilePath': 'app/Main.hs', 'spanToLine': 7}]], 'seq': '6ee8d949-82bd-491d-8b79-ffcaa3e65fde', 'tag': 'ResponseGetSpanInfo'} 13 | test_settings = Settings("none", [], False) 14 | source_errors = {'seq': 'd0599c00-0b77-441c-8947-b3882cab298c', 'tag': 'ResponseGetSourceErrors', 'contents': [{'errorSpan': {'tag': 'ProperSpan', 'contents': {'spanFromColumn': 22, 'spanFromLine': 11, 'spanFilePath': 'src/Lib.hs', 'spanToColumn': 28, 'spanToLine': 11}}, 'errorKind': 'KindError', 'errorMsg': 'Couldn\'t match expected type ‘Integer’ with actual type ‘[Char]’\nIn the first argument of ‘greet’, namely ‘"You!"’\nIn the second argument of ‘($)’, namely ‘greet "You!"’\nIn a stmt of a \'do\' block: putStrLn $ greet "You!"'}, {'errorSpan': {'tag': 'ProperSpan', 'contents': {'spanFromColumn': 24, 'spanFromLine': 15, 'spanFilePath': 'src/Lib.hs', 'spanToColumn': 25, 'spanToLine': 15}}, 'errorKind': 'KindError', 'errorMsg': 'Couldn\'t match expected type ‘[Char]’ with actual type ‘Integer’\nIn the second argument of ‘(++)’, namely ‘s’\nIn the expression: "Hello, " ++ s'}]} 15 | many_completions = {'tag': 'ResponseGetAutocompletion', 'contents': [{'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.List', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'GHC.OldList', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '!!', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Data.List', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 4, 'spanToLine': 4, 'spanFromColumn': 1, 'spanToColumn': 17}}, 'idImportQual': ''}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Base', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'Data.Function', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': '(a -> b) -> a -> b', 'idName': '$', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'WiredIn', 'contents': []}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Base', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '$!', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 1, 'spanToLine': 1, 'spanFromColumn': 1, 'spanToColumn': 1}}, 'idImportQual': ''}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Classes', 'modulePackage': {'packageName': 'ghc-prim', 'packageKey': 'ghc-prim', 'packageVersion': '0.4.0.0'}}, 'idHomeModule': {'moduleName': 'Data.Bool', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '&&', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 1, 'spanToLine': 1, 'spanFromColumn': 1, 'spanToColumn': 1}}, 'idImportQual': ''}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Num', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '*', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 1, 'spanToLine': 1, 'spanFromColumn': 1, 'spanToColumn': 1}}, 'idImportQual': ''}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Float', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '**', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 1, 'spanToLine': 1, 'spanFromColumn': 1, 'spanToColumn': 1}}, 'idImportQual': ''}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Base', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'Control.Applicative', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '*>', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 1, 'spanToLine': 1, 'spanFromColumn': 1, 'spanToColumn': 1}}, 'idImportQual': ''}}, {'idProp': {'idSpace': 'VarName', 'idDefinedIn': {'moduleName': 'GHC.Num', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idHomeModule': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idType': None, 'idName': '+', 'idDefSpan': {'tag': 'TextSpan', 'contents': ''}}, 'idScope': {'tag': 'Imported', 'idImportedFrom': {'moduleName': 'Prelude', 'modulePackage': {'packageName': 'base', 'packageKey': 'base', 'packageVersion': '4.8.1.0'}}, 'idImportSpan': {'tag': 'ProperSpan', 'contents': {'spanFilePath': 'app/Main.hs', 'spanFromLine': 1, 'spanToLine': 1, 'spanFromColumn': 1, 'spanToColumn': 1}}, 'idImportQual': ''}}]} 16 | readFile_exp_types = {'tag': 'ResponseGetExpTypes', 'contents': [['FilePath -> IO String', {'spanToColumn': 25, 'spanToLine': 10, 'spanFromColumn': 17, 'spanFromLine': 10, 'spanFilePath': 'src/Lib.hs'}], ['IO String', {'spanToColumn': 36, 'spanToLine': 10, 'spanFromColumn': 17, 'spanFromLine': 10, 'spanFilePath': 'src/Lib.hs'}], ['IO ()', {'spanToColumn': 28, 'spanToLine': 11, 'spanFromColumn': 12, 'spanFromLine': 9, 'spanFilePath': 'src/Lib.hs'}]], 'seq': 'fd3eb2a5-e390-4ad7-be72-8b2e82441a95'} 17 | status_progress_restart = {'contents': {'contents': [], 'tag': 'UpdateStatusRequiredRestart'}, 'tag': 'ResponseUpdateSession'} 18 | status_progress_1 = {'contents': {'contents': {'progressParsedMsg': 'Compiling Lib', 'progressNumSteps': 2, 'progressStep': 1, 'progressOrigMsg': '[1 of 2] Compiling Lib ( /Users/tomv/Projects/Personal/haskell/helloworld/src/Lib.hs, interpreted )'}, 'tag': 'UpdateStatusProgress'}, 'tag': 'ResponseUpdateSession'} 19 | status_progress_2 = {'contents': {'contents': {'progressParsedMsg': 'Compiling Main', 'progressNumSteps': 2, 'progressStep': 2, 'progressOrigMsg': '[2 of 2] Compiling Main ( /Users/tomv/Projects/Personal/haskell/helloworld/app/Main.hs, interpreted )'}, 'tag': 'UpdateStatusProgress'}, 'tag': 'ResponseUpdateSession'} 20 | status_progress_done = {'contents': {'contents': [], 'tag': 'UpdateStatusDone'}, 'tag': 'ResponseUpdateSession'} 21 | -------------------------------------------------------------------------------- /test/fakebackend.py: -------------------------------------------------------------------------------- 1 | from .data import exp_types_response 2 | 3 | def seq_response(seq_id, contents): 4 | contents['seq']= seq_id 5 | return contents 6 | 7 | 8 | def make_response(seq_id, contents): 9 | return {'seq': seq_id, 'contents': contents} 10 | 11 | class FakeBackend(): 12 | """ 13 | Fakes responses from the stack-ide process 14 | Override responses by passing in a dict keyed by tag 15 | """ 16 | 17 | def __init__(self, responses={}): 18 | self.responses = responses 19 | if self.responses is None: 20 | raise Exception('stopthat!') 21 | 22 | def send_request(self, req): 23 | 24 | if self.handler: 25 | self.return_test_data(req) 26 | 27 | def return_test_data(self, req): 28 | 29 | tag = req.get('tag') 30 | seq_id = req.get('seq') 31 | 32 | # overrides 33 | if self.responses is None: 34 | raise Exception('wtf!') 35 | override = self.responses.get(tag) 36 | if override: 37 | self.handler(seq_response(seq_id, override)) 38 | return 39 | 40 | # default responses 41 | if tag == 'RequestUpdateSession': 42 | return 43 | if tag == 'RequestShutdownSession': 44 | return 45 | if tag == 'RequestGetSourceErrors': 46 | self.handler(make_response(seq_id, [])) 47 | return 48 | if tag == 'RequestGetExpTypes': 49 | self.handler(seq_response(seq_id, exp_types_response)) 50 | return 51 | else: 52 | raise Exception(tag) 53 | -------------------------------------------------------------------------------- /test/mocks.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest.mock import Mock, MagicMock 3 | 4 | from stack_ide import StackIDE 5 | from stack_ide_manager import StackIDEManager 6 | from .fakebackend import FakeBackend 7 | from settings import Settings 8 | 9 | cur_dir = os.path.dirname(os.path.realpath(__file__)) 10 | test_settings = Settings("none", [], False) 11 | 12 | def mock_window(paths=[]): 13 | window = MagicMock() 14 | window.folders = Mock(return_value=paths) 15 | window.id = Mock(return_value=1234) 16 | window.run_command = Mock(return_value=None) 17 | return window 18 | 19 | def mock_view(file_path, window): 20 | view = MagicMock() 21 | view.file_name = Mock(return_value=os.path.join(window.folders()[0], file_path)) 22 | view.match_selector = Mock(return_value=True) 23 | window.active_view = Mock(return_value=view) 24 | window.find_open_file = Mock(return_value=view) 25 | window.views = Mock(return_value=[view]) 26 | view.window = Mock(return_value=window) 27 | region = MagicMock() 28 | region.begin = Mock(return_value=4) 29 | region.end = Mock(return_value=4) 30 | view.sel = Mock(return_value=[region]) 31 | view.rowcol = Mock(return_value=(0, 0)) 32 | view.text_point = Mock(return_value=4) 33 | return view 34 | 35 | def setup_fake_backend(window, responses={}): 36 | backend = FakeBackend(responses) 37 | instance = StackIDE(window, test_settings, backend) 38 | backend.handler = instance.handle_response 39 | StackIDEManager.ide_backend_instances[ 40 | window.id()] = instance 41 | return backend 42 | 43 | def setup_mock_backend(window): 44 | backend = MagicMock() 45 | instance = StackIDE(window, test_settings, backend) 46 | # backend.handler = instance.handle_response 47 | StackIDEManager.ide_backend_instances[ 48 | window.id()] = instance 49 | return backend 50 | 51 | 52 | def default_mock_window(): 53 | """ 54 | Returns a (window, view) tuple pointing to /projects/helloworld/src/Main.hs 55 | """ 56 | window = mock_window([cur_dir + '/projects/helloworld']) 57 | view = mock_view('src/Main.hs', window) 58 | return (window, view) 59 | -------------------------------------------------------------------------------- /test/projects/cabal_project/cabal_project.cabal: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukexi/stack-ide-sublime/89c7a1b342183a107ec1e188902e828eb87c41e7/test/projects/cabal_project/cabal_project.cabal -------------------------------------------------------------------------------- /test/projects/cabalfile_wrong_project/cabal_project.cabal: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukexi/stack-ide-sublime/89c7a1b342183a107ec1e188902e828eb87c41e7/test/projects/cabalfile_wrong_project/cabal_project.cabal -------------------------------------------------------------------------------- /test/projects/cabalfile_wrong_project/stack.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukexi/stack-ide-sublime/89c7a1b342183a107ec1e188902e828eb87c41e7/test/projects/cabalfile_wrong_project/stack.yaml -------------------------------------------------------------------------------- /test/projects/empty_project/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukexi/stack-ide-sublime/89c7a1b342183a107ec1e188902e828eb87c41e7/test/projects/empty_project/.empty -------------------------------------------------------------------------------- /test/projects/helloworld/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /test/projects/helloworld/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /test/projects/helloworld/helloworld.cabal: -------------------------------------------------------------------------------- 1 | name: helloworld 2 | version: 0.1.0.0 3 | synopsis: Simple project template from stack 4 | description: Please see README.md 5 | homepage: http://github.com/githubuser/helloworld#readme 6 | license: BSD3 7 | license-file: LICENSE 8 | author: Author name here 9 | maintainer: example@example.com 10 | copyright: 2010 Author Here 11 | category: Web 12 | build-type: Simple 13 | cabal-version: >=1.10 14 | 15 | executable helloworld 16 | hs-source-dirs: src 17 | main-is: Main.hs 18 | default-language: Haskell2010 19 | build-depends: base >= 4.7 && < 5 20 | -------------------------------------------------------------------------------- /test/projects/helloworld/src/Main.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | main :: IO () 4 | main = do 5 | putStrLn "hello world" 6 | -------------------------------------------------------------------------------- /test/projects/helloworld/stack.yaml: -------------------------------------------------------------------------------- 1 | flags: {} 2 | packages: 3 | - '.' 4 | extra-deps: [] 5 | resolver: lts-3.1 6 | -------------------------------------------------------------------------------- /test/projects/stack_project/stack.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukexi/stack-ide-sublime/89c7a1b342183a107ec1e188902e828eb87c41e7/test/projects/stack_project/stack.yaml -------------------------------------------------------------------------------- /test/projects/stack_project/stack_project.cabal: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukexi/stack-ide-sublime/89c7a1b342183a107ec1e188902e828eb87c41e7/test/projects/stack_project/stack_project.cabal -------------------------------------------------------------------------------- /test/stubs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukexi/stack-ide-sublime/89c7a1b342183a107ec1e188902e828eb87c41e7/test/stubs/__init__.py -------------------------------------------------------------------------------- /test/stubs/sublime.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | current_status = "" 4 | current_error = "" 5 | 6 | def status_message(msg): 7 | global current_status 8 | current_status = msg 9 | 10 | def error_message(msg): 11 | global current_error 12 | current_error = msg 13 | 14 | def set_timeout_async(fn, delay): 15 | fn() 16 | 17 | def set_timeout(fn, delay): 18 | fn() 19 | 20 | def load_settings(name): 21 | return Settings() 22 | 23 | class Settings(): 24 | 25 | def add_on_change(self, key, func): 26 | pass 27 | 28 | def get(self, key, default): 29 | return default 30 | 31 | 32 | class FakeWindow(): 33 | 34 | def __init__(self, folder): 35 | self._folders = [folder] 36 | self._id = uuid.uuid4() 37 | 38 | def id(self): 39 | return self._id 40 | 41 | def folders(self): 42 | return self._folders 43 | 44 | def run_command(self, command, args): 45 | pass 46 | 47 | # def create_output_panel(): 48 | # return None 49 | 50 | fake_windows = [] 51 | 52 | ENCODED_POSITION = 1 #flag used for window.open_file 53 | DRAW_OUTLINED = 2 # flag used for view.add_regions 54 | 55 | clipboard = None 56 | 57 | def create_window(path): 58 | global fake_windows 59 | window = FakeWindow(path) 60 | fake_windows.append(window) 61 | return window 62 | 63 | def add_window(window): 64 | fake_windows.append(window) 65 | 66 | def destroy_windows(): 67 | global fake_windows 68 | fake_windows = [] 69 | 70 | def set_clipboard(text): 71 | global clipboard 72 | clipboard = text 73 | 74 | def windows(): 75 | global fake_windows 76 | return fake_windows 77 | 78 | class Region(): 79 | 80 | def __init__(self, begin, end): 81 | self._begin = begin 82 | self._end = end 83 | 84 | def begin(self): 85 | return self._begin 86 | 87 | def end(self): 88 | return self._end 89 | -------------------------------------------------------------------------------- /test/stubs/sublime_plugin.py: -------------------------------------------------------------------------------- 1 | 2 | class TextCommand(): 3 | 4 | def __init__(self): 5 | pass 6 | 7 | class EventListener(): 8 | 9 | def __init__(self): 10 | pass 11 | 12 | class WindowCommand(): 13 | 14 | def __init__(self): 15 | pass 16 | 17 | class ApplicationCommand(): 18 | 19 | def __init__(self): 20 | pass 21 | -------------------------------------------------------------------------------- /test/test_commands.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import MagicMock, Mock, ANY 3 | import stack_ide as stackide 4 | from .mocks import cur_dir, default_mock_window, setup_fake_backend 5 | from text_commands import ClearErrorPanelCommand, AppendToErrorPanelCommand, ShowHsTypeAtCursorCommand, ShowHsInfoAtCursorCommand, CopyHsTypeAtCursorCommand, GotoDefinitionAtCursorCommand 6 | from .stubs import sublime 7 | from .data import type_info, someFunc_span_info, putStrLn_span_info 8 | 9 | 10 | class CommandTests(unittest.TestCase): 11 | 12 | def setUp(self): 13 | stackide.stack_ide_loadtargets = Mock(return_value=['app/Main.hs', 'src/Lib.hs']) 14 | 15 | def test_can_clear_panel(self): 16 | cmd = ClearErrorPanelCommand() 17 | cmd.view = MagicMock() 18 | cmd.run(None) 19 | cmd.view.erase.assert_called_with(ANY, ANY) 20 | 21 | def test_can_update_panel(self): 22 | cmd = AppendToErrorPanelCommand() 23 | cmd.view = MagicMock() 24 | cmd.view.size = Mock(return_value=0) 25 | cmd.run(None, 'message') 26 | cmd.view.insert.assert_called_with(ANY, 0, "message\n\n") 27 | 28 | def test_can_show_type_at_cursor(self): 29 | 30 | cmd = ShowHsTypeAtCursorCommand() 31 | (window, view) = default_mock_window() 32 | cmd.view = view 33 | setup_fake_backend(window) 34 | 35 | cmd.run(None) 36 | cmd.view.show_popup.assert_called_with(type_info) 37 | 38 | def test_can_copy_type_at_cursor(self): 39 | 40 | cmd = CopyHsTypeAtCursorCommand() 41 | (window, view) = default_mock_window() 42 | cmd.view = view 43 | setup_fake_backend(window) 44 | 45 | cmd.run(None) 46 | 47 | self.assertEqual(sublime.clipboard, type_info) 48 | 49 | def test_can_request_show_info_at_cursor(self): 50 | 51 | cmd = ShowHsInfoAtCursorCommand() 52 | (window, view) = default_mock_window() 53 | cmd.view = view 54 | 55 | setup_fake_backend(window, {'RequestGetSpanInfo': someFunc_span_info}) 56 | 57 | cmd.run(None) 58 | cmd.view.show_popup.assert_called_with("someFunc :: IO () (Defined in src/Lib.hs:9:1)") 59 | 60 | def test_show_info_from_module(self): 61 | 62 | cmd = ShowHsInfoAtCursorCommand() 63 | (window, view) = default_mock_window() 64 | cmd.view = view 65 | 66 | 67 | setup_fake_backend(window, {'RequestGetSpanInfo':putStrLn_span_info}) 68 | 69 | cmd.run(None) 70 | 71 | cmd.view.show_popup.assert_called_with("putStrLn :: String -> IO () (Imported from Prelude)") 72 | 73 | def test_goto_definition_at_cursor(self): 74 | 75 | cmd = GotoDefinitionAtCursorCommand() 76 | (window, view) = default_mock_window() 77 | cmd.view = view 78 | 79 | setup_fake_backend(window, {'RequestGetSpanInfo': someFunc_span_info}) 80 | 81 | cmd.run(None) 82 | 83 | window.open_file.assert_called_with(cur_dir + "/projects/helloworld/src/Lib.hs:9:1", sublime.ENCODED_POSITION) 84 | 85 | def test_goto_definition_of_module(self): 86 | 87 | cmd = GotoDefinitionAtCursorCommand() 88 | (window, view) = default_mock_window() 89 | cmd.view = view 90 | 91 | cmd._handle_response(putStrLn_span_info.get('contents')) 92 | 93 | self.assertEqual("Cannot navigate to putStrLn, it is imported from Prelude", sublime.current_status) 94 | -------------------------------------------------------------------------------- /test/test_listeners.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock, ANY 3 | from event_listeners import StackIDESaveListener, StackIDETypeAtCursorHandler, StackIDEAutocompleteHandler 4 | from req import Req 5 | from .stubs import sublime 6 | from .mocks import default_mock_window, setup_fake_backend, setup_mock_backend 7 | from settings import Settings 8 | import stack_ide 9 | import utility as util 10 | from .data import many_completions 11 | 12 | test_settings = Settings("none", [], False) 13 | type_info = "FilePath -> IO String" 14 | span = { 15 | "spanFromLine": 1, 16 | "spanFromColumn": 1, 17 | "spanToLine": 1, 18 | "spanToColumn": 5 19 | } 20 | exp_types_response = {"tag": "", "contents": [[type_info, span]]} 21 | request_include_targets = {'contents': [{'contents': {'contents': ['src/Main.hs'], 'tag': 'TargetsInclude'}, 'tag': 'RequestUpdateTargets'}], 'tag': 'RequestUpdateSession'} 22 | 23 | class ListenerTests(unittest.TestCase): 24 | 25 | def setUp(self): 26 | stack_ide.stack_ide_loadtargets = Mock(return_value=['app/Main.hs', 'src/Lib.hs']) 27 | 28 | def test_requests_update_on_save(self): 29 | listener = StackIDESaveListener() 30 | 31 | (window, view) = default_mock_window() 32 | backend = setup_mock_backend(window) 33 | backend.send_request.reset_mock() 34 | 35 | listener.on_post_save(view) 36 | backend.send_request.assert_called_with(ANY) 37 | 38 | 39 | 40 | def test_ignores_non_haskell_views(self): 41 | listener = StackIDESaveListener() 42 | (window, view) = default_mock_window() 43 | view.match_selector.return_value = False 44 | backend = setup_mock_backend(window) 45 | 46 | backend.send_request.reset_mock() 47 | 48 | listener.on_post_save(view) 49 | 50 | backend.send_request.assert_not_called() 51 | 52 | 53 | def test_type_at_cursor_tests(self): 54 | listener = StackIDETypeAtCursorHandler() 55 | (window, view) = default_mock_window() 56 | setup_fake_backend(window, exp_types_response) 57 | 58 | listener.on_selection_modified(view) 59 | 60 | view.set_status.assert_called_with("type_at_cursor", type_info) 61 | view.add_regions.assert_called_with("type_at_cursor", ANY, "storage.type", "", sublime.DRAW_OUTLINED) 62 | 63 | def test_request_completions(self): 64 | 65 | listener = StackIDEAutocompleteHandler() 66 | (window, view) = default_mock_window() 67 | view.settings().get = Mock(return_value=False) 68 | backend = setup_mock_backend(window) 69 | 70 | listener.on_query_completions(view, 'm', []) #locations not used. 71 | 72 | req = Req.get_autocompletion(filepath=util.relative_view_file_name(view),prefix="m") 73 | req['seq'] = ANY 74 | backend.send_request.assert_called_with(req) 75 | 76 | def test_returns_completions(self): 77 | listener = StackIDEAutocompleteHandler() 78 | (window, view) = default_mock_window() 79 | view.settings().get = Mock(side_effect=[False, True]) 80 | setup_fake_backend(window, {'RequestGetAutocompletion': many_completions}) 81 | 82 | completions = listener.on_query_completions(view, 'm', []) #locations not used. 83 | 84 | self.assertEqual(8, len(completions)) 85 | self.assertEqual(['!!\t\tData.List', '!!'], completions[0]) 86 | 87 | # in live situations on_query_completions returns [] first while we retrieve results 88 | # here we make sure that the re-trigger calls are still in place 89 | view.run_command.assert_any_call('hide_auto_complete') 90 | view.run_command.assert_any_call('auto_complete', ANY) 91 | 92 | -------------------------------------------------------------------------------- /test/test_response.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import response as res 3 | from .data import source_errors, status_progress_1, status_progress_2, status_progress_done, status_progress_restart, many_completions, readFile_exp_types 4 | 5 | class ParsingTests(unittest.TestCase): 6 | 7 | def test_parse_source_errors_empty(self): 8 | errors = res.parse_source_errors([]) 9 | self.assertEqual(0, len(list(errors))) 10 | 11 | def test_parse_source_errors_error(self): 12 | errors = list(res.parse_source_errors(source_errors.get('contents'))) 13 | self.assertEqual(2, len(errors)) 14 | err1, err2 = errors 15 | self.assertEqual(err1.kind, 'KindError') 16 | self.assertRegex(err1.msg, "Couldn\'t match expected type ‘Integer’") 17 | self.assertEqual(err1.span.filePath, 'src/Lib.hs') 18 | self.assertEqual(err1.span.fromLine, 11) 19 | self.assertEqual(err1.span.fromColumn, 22) 20 | 21 | self.assertEqual(err2.kind, 'KindError') 22 | self.assertRegex(err2.msg, "Couldn\'t match expected type ‘\[Char\]’") 23 | self.assertEqual(err2.span.filePath, 'src/Lib.hs') 24 | self.assertEqual(err2.span.fromLine, 15) 25 | self.assertEqual(err2.span.fromColumn, 24) 26 | 27 | def test_parse_exp_types_empty(self): 28 | exp_types = res.parse_exp_types([]) 29 | self.assertEqual(0, len(list(exp_types))) 30 | 31 | def test_parse_exp_types_readFile(self): 32 | exp_types = list(res.parse_exp_types(readFile_exp_types.get('contents'))) 33 | self.assertEqual(3, len(exp_types)) 34 | (type, span) = exp_types[0] 35 | 36 | self.assertEqual('FilePath -> IO String', type) 37 | self.assertEqual('src/Lib.hs', span.filePath) 38 | 39 | def test_parse_completions_empty(self): 40 | self.assertEqual([], list(res.parse_autocompletions([]))) 41 | 42 | def test_parse_completions(self): 43 | completions = list(res.parse_autocompletions(many_completions.get('contents'))) 44 | self.assertEqual(8, len(completions)) 45 | (prop, scope) = completions[0] 46 | self.assertEqual('!!', prop.name) 47 | self.assertEqual(None, prop.type) 48 | self.assertEqual('Data.List', scope.importedFrom.module) 49 | 50 | def test_parse_update_session(self): 51 | 52 | self.assertEqual('Starting session...', res.parse_update_session(status_progress_restart.get('contents'))) 53 | self.assertEqual('Compiling Lib', res.parse_update_session(status_progress_1.get('contents'))) 54 | self.assertEqual('Compiling Main', res.parse_update_session(status_progress_2.get('contents'))) 55 | self.assertEqual(' ', res.parse_update_session(status_progress_done.get('contents'))) 56 | -------------------------------------------------------------------------------- /test/test_stack_ide_manager.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import MagicMock, Mock 3 | from stack_ide_manager import NoStackIDE, StackIDEManager, configure_instance 4 | import stack_ide 5 | from .mocks import mock_window, cur_dir 6 | from .stubs import sublime 7 | from .fakebackend import FakeBackend 8 | from .data import test_settings 9 | from log import Log 10 | from req import Req 11 | import watchdog as wd 12 | 13 | 14 | class WatchdogTests(unittest.TestCase): 15 | 16 | def test_managed_by_plugin_events(self): 17 | 18 | self.assertIsNone(wd.watchdog) 19 | 20 | wd.plugin_loaded() 21 | 22 | self.assertIsNotNone(wd.watchdog) 23 | 24 | wd.plugin_unloaded() 25 | 26 | self.assertIsNone(wd.watchdog) 27 | 28 | 29 | class StackIDEManagerTests(unittest.TestCase): 30 | 31 | 32 | def test_defaults(self): 33 | 34 | StackIDEManager.check_windows() 35 | self.assertEqual(0, len(StackIDEManager.ide_backend_instances)) 36 | 37 | 38 | def test_creates_initial_window(self): 39 | 40 | sublime.create_window('.') 41 | StackIDEManager.check_windows() 42 | self.assertEqual(1, len(StackIDEManager.ide_backend_instances)) 43 | sublime.destroy_windows() 44 | 45 | def test_monitors_closed_windows(self): 46 | 47 | sublime.create_window('.') 48 | StackIDEManager.check_windows() 49 | self.assertEqual(1, len(StackIDEManager.ide_backend_instances)) 50 | sublime.destroy_windows() 51 | StackIDEManager.check_windows() 52 | self.assertEqual(0, len(StackIDEManager.ide_backend_instances)) 53 | 54 | def test_monitors_new_windows(self): 55 | 56 | StackIDEManager.check_windows() 57 | self.assertEqual(0, len(StackIDEManager.ide_backend_instances)) 58 | sublime.create_window('.') 59 | StackIDEManager.check_windows() 60 | self.assertEqual(1, len(StackIDEManager.ide_backend_instances)) 61 | sublime.destroy_windows() 62 | 63 | def test_retains_live_instances(self): 64 | 65 | window = mock_window(['.']) 66 | sublime.add_window(window) 67 | 68 | StackIDEManager.check_windows() 69 | self.assertEqual(1, len(StackIDEManager.ide_backend_instances)) 70 | 71 | # substitute a 'live' instance 72 | instance = stack_ide.StackIDE(window, test_settings, FakeBackend()) 73 | StackIDEManager.ide_backend_instances[window.id()] = instance 74 | 75 | # instance should still exist. 76 | StackIDEManager.check_windows() 77 | self.assertEqual(1, len(StackIDEManager.ide_backend_instances)) 78 | self.assertEqual(instance, StackIDEManager.ide_backend_instances[window.id()]) 79 | 80 | sublime.destroy_windows() 81 | 82 | def test_kills_live_orphans(self): 83 | window = sublime.create_window('.') 84 | StackIDEManager.check_windows() 85 | self.assertEqual(1, len(StackIDEManager.ide_backend_instances)) 86 | 87 | # substitute a 'live' instance 88 | backend = MagicMock() 89 | stack_ide.stack_ide_loadtargets = Mock(return_value=['app/Main.hs', 'src/Lib.hs']) 90 | instance = stack_ide.StackIDE(window, test_settings, backend) 91 | StackIDEManager.ide_backend_instances[window.id()] = instance 92 | 93 | # close the window 94 | sublime.destroy_windows() 95 | 96 | # instance should be killed 97 | StackIDEManager.check_windows() 98 | self.assertEqual(0, len(StackIDEManager.ide_backend_instances)) 99 | self.assertFalse(instance.is_alive) 100 | backend.send_request.assert_called_with(Req.get_shutdown()) 101 | 102 | 103 | def test_retains_existing_instances(self): 104 | StackIDEManager.check_windows() 105 | self.assertEqual(0, len(StackIDEManager.ide_backend_instances)) 106 | sublime.create_window('.') 107 | StackIDEManager.check_windows() 108 | self.assertEqual(1, len(StackIDEManager.ide_backend_instances)) 109 | StackIDEManager.check_windows() 110 | self.assertEqual(1, len(StackIDEManager.ide_backend_instances)) 111 | sublime.destroy_windows() 112 | 113 | def test_reset(self): 114 | window = mock_window(['.']) 115 | sublime.add_window(window) 116 | 117 | StackIDEManager.check_windows() 118 | self.assertEqual(1, len(StackIDEManager.ide_backend_instances)) 119 | 120 | # substitute a 'live' instance 121 | backend = MagicMock() 122 | stack_ide.stack_ide_loadtargets = Mock(return_value=['app/Main.hs', 'src/Lib.hs']) 123 | instance = stack_ide.StackIDE(window, test_settings, backend) 124 | StackIDEManager.ide_backend_instances[window.id()] = instance 125 | 126 | StackIDEManager.reset() 127 | 128 | # instances should be shut down. 129 | self.assertEqual(1, len(StackIDEManager.ide_backend_instances)) 130 | self.assertFalse(instance.is_alive) 131 | backend.send_request.assert_called_with(Req.get_shutdown()) 132 | 133 | sublime.destroy_windows() 134 | 135 | 136 | 137 | 138 | class LaunchTests(unittest.TestCase): 139 | 140 | # launching Stack IDE is a function that should result in a 141 | # Stack IDE instance (null object or live) 142 | # the null object should contain the reason why the launch failed. 143 | def setUp(self): 144 | Log._set_verbosity("none") 145 | 146 | 147 | def test_launch_window_without_folder(self): 148 | instance = configure_instance(mock_window([]), test_settings) 149 | self.assertIsInstance(instance, NoStackIDE) 150 | self.assertRegex(instance.reason, "No folder to monitor.*") 151 | 152 | def test_launch_window_with_empty_folder(self): 153 | instance = configure_instance( 154 | mock_window([cur_dir + '/projects/empty_project']), test_settings) 155 | self.assertIsInstance(instance, NoStackIDE) 156 | self.assertRegex(instance.reason, "No cabal file found.*") 157 | 158 | def test_launch_window_with_cabal_folder(self): 159 | instance = configure_instance( 160 | mock_window([cur_dir + '/projects/cabal_project']), test_settings) 161 | self.assertIsInstance(instance, NoStackIDE) 162 | self.assertRegex(instance.reason, "No stack.yaml in path.*") 163 | 164 | def test_launch_window_with_wrong_cabal_file(self): 165 | instance = configure_instance( 166 | mock_window([cur_dir + '/projects/cabalfile_wrong_project']), test_settings) 167 | self.assertIsInstance(instance, NoStackIDE) 168 | self.assertRegex( 169 | instance.reason, "cabalfile_wrong_project.cabal not found.*") 170 | 171 | @unittest.skip("Actually starts a stack ide, slow and won't work on Travis") 172 | def test_launch_window_with_helloworld_project(self): 173 | instance = configure_instance( 174 | mock_window([cur_dir + '/projects/helloworld']), test_settings) 175 | self.assertIsInstance(instance, stack_ide.StackIDE) 176 | instance.end() 177 | 178 | def test_launch_window_stack_not_found(self): 179 | 180 | stack_ide.stack_ide_start = Mock(side_effect=FileNotFoundError()) 181 | instance = configure_instance( 182 | mock_window([cur_dir + '/projects/helloworld']), test_settings) 183 | self.assertIsInstance(instance, NoStackIDE) 184 | self.assertRegex( 185 | instance.reason, "instance init failed -- stack not found") 186 | self.assertRegex(sublime.current_error, "Could not find program 'stack'!") 187 | 188 | def test_launch_window_stack_unknown_error(self): 189 | 190 | stack_ide.stack_ide_start = Mock(side_effect=Exception()) 191 | instance = configure_instance( 192 | mock_window([cur_dir + '/projects/helloworld']), test_settings) 193 | self.assertIsInstance(instance, NoStackIDE) 194 | self.assertRegex( 195 | instance.reason, "instance init failed -- unknown error") 196 | -------------------------------------------------------------------------------- /test/test_stackide.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock, MagicMock, patch 3 | import stack_ide as stackide 4 | from .stubs import sublime 5 | from .fakebackend import FakeBackend 6 | from .mocks import mock_window, cur_dir 7 | from settings import Settings 8 | from .data import status_progress_1 9 | from req import Req 10 | 11 | test_settings = Settings("none", [], False) 12 | 13 | @patch('stack_ide.stack_ide_loadtargets', return_value=['app/Main.hs', 'src/Lib.hs']) 14 | class StackIDETests(unittest.TestCase): 15 | 16 | def test_can_create(self, loadtargets_mock): 17 | instance = stackide.StackIDE( 18 | mock_window([cur_dir + '/mocks/helloworld/']), test_settings, FakeBackend()) 19 | self.assertIsNotNone(instance) 20 | self.assertTrue(instance.is_active) 21 | self.assertTrue(instance.is_alive) 22 | 23 | # it got the load targets 24 | self.assertEqual(2, len(instance.include_targets)) 25 | 26 | # it should also have called get source errors, 27 | # but FakeBackend sends no errors back by default. 28 | 29 | 30 | def test_can_send_source_errors_request(self, loadtargets_mock): 31 | backend = FakeBackend() 32 | backend.send_request = Mock() 33 | instance = stackide.StackIDE( 34 | mock_window([cur_dir + '/mocks/helloworld/']), test_settings, backend) 35 | self.assertIsNotNone(instance) 36 | self.assertTrue(instance.is_active) 37 | self.assertTrue(instance.is_alive) 38 | req = Req.get_source_errors() 39 | instance.send_request(req) 40 | backend.send_request.assert_called_with(req) 41 | 42 | def test_handle_welcome_stack_ide_outdated(self, loadtargets_mock): 43 | 44 | backend = MagicMock() 45 | welcome = { 46 | "tag": "ResponseWelcome", 47 | "contents": [0, 0, 0] 48 | } 49 | 50 | instance = stackide.StackIDE(mock_window([cur_dir + '/projects/helloworld/']), test_settings, backend) 51 | instance.handle_response(welcome) 52 | self.assertEqual(sublime.current_error, "Please upgrade stack-ide to a newer version.") 53 | 54 | 55 | def test_handle_progress_update(self, loadtargets_mock): 56 | backend = MagicMock() 57 | instance = stackide.StackIDE(mock_window([cur_dir + '/projects/helloworld/']), test_settings, backend) 58 | instance.handle_response(status_progress_1) 59 | self.assertEqual(sublime.current_status, "Compiling Lib") 60 | 61 | 62 | def test_can_shutdown(self, loadtargets_mock): 63 | backend = FakeBackend() 64 | backend.send_request = Mock() 65 | instance = stackide.StackIDE( 66 | mock_window([cur_dir + '/projects/helloworld/']), test_settings, backend) 67 | self.assertIsNotNone(instance) 68 | self.assertTrue(instance.is_active) 69 | self.assertTrue(instance.is_alive) 70 | instance.end() 71 | self.assertFalse(instance.is_active) 72 | self.assertFalse(instance.is_alive) 73 | backend.send_request.assert_called_with( 74 | Req.get_shutdown()) 75 | 76 | -------------------------------------------------------------------------------- /test/test_utility.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from test.mocks import mock_view, mock_window, cur_dir 3 | import utility 4 | from .stubs import sublime 5 | 6 | class UtilTests(unittest.TestCase): 7 | 8 | def test_get_relative_filename(self): 9 | window = mock_window([cur_dir + '/projects/helloworld']) 10 | view = mock_view('src/Main.hs', window) 11 | self.assertEqual('src/Main.hs', utility.relative_view_file_name(view)) 12 | 13 | def test_is_haskell_view(self): 14 | window = mock_window([cur_dir + '/projects/helloworld']) 15 | view = mock_view('src/Main.hs', window) 16 | self.assertTrue(utility.is_haskell_view(view)) 17 | 18 | def test_span_from_view_selection(self): 19 | window = mock_window([cur_dir + '/projects/helloworld']) 20 | view = mock_view('src/Main.hs', window) 21 | span = utility.span_from_view_selection(view) 22 | self.assertEqual(1, span['spanFromLine']) 23 | self.assertEqual(1, span['spanToLine']) 24 | self.assertEqual(1, span['spanFromColumn']) 25 | self.assertEqual(1, span['spanToColumn']) 26 | self.assertEqual('src/Main.hs', span['spanFilePath']) 27 | 28 | def test_complaints_not_repeated(self): 29 | utility.complain('complaint', 'waaaah') 30 | self.assertEqual(sublime.current_error, 'waaaah') 31 | utility.complain('complaint', 'waaaah 2') 32 | self.assertEqual(sublime.current_error, 'waaaah') 33 | utility.reset_complaints() 34 | utility.complain('complaint', 'waaaah 2') 35 | self.assertEqual(sublime.current_error, 'waaaah 2') 36 | 37 | -------------------------------------------------------------------------------- /test/test_win.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import MagicMock, Mock, ANY 3 | from win import Win 4 | from .stubs import sublime 5 | from .mocks import cur_dir, default_mock_window 6 | from utility import relative_view_file_name 7 | 8 | def create_source_error(filePath, kind, message): 9 | return { 10 | "errorKind": kind, 11 | "errorMsg": message, 12 | "errorSpan": { 13 | "tag": "ProperSpan", 14 | "contents": { 15 | "spanFilePath": filePath, 16 | "spanFromLine": 1, 17 | "spanFromColumn": 1, 18 | "spanToLine": 1, 19 | "spanToColumn": 5 20 | } 21 | } 22 | } 23 | 24 | 25 | class WinTests(unittest.TestCase): 26 | 27 | def test_highlight_type_clear(self): 28 | (window, view) = default_mock_window() 29 | 30 | Win(window).highlight_type([]) 31 | 32 | view.set_status.assert_called_with("type_at_cursor", "") 33 | view.add_regions.assert_called_with("type_at_cursor", [], "storage.type", "", sublime.DRAW_OUTLINED) 34 | 35 | def test_highlight_no_errors(self): 36 | 37 | (window, view) = default_mock_window() 38 | 39 | panel = MagicMock() 40 | window.create_output_panel = Mock(return_value=panel) 41 | 42 | errors = [] 43 | Win(window).handle_source_errors(errors) 44 | 45 | # panel recreated 46 | window.create_output_panel.assert_called_with("hide_errors") 47 | window.run_command.assert_any_call("hide_panel", {"panel": "output.hide_errors"}) 48 | panel.run_command.assert_called_with("clear_error_panel") 49 | panel.set_read_only.assert_any_call(False) 50 | 51 | # regions created in view 52 | view.add_regions.assert_any_call("errors", [], "invalid", "dot", sublime.DRAW_OUTLINED) 53 | view.add_regions.assert_any_call("warnings", [], "comment", "dot", sublime.DRAW_OUTLINED) 54 | 55 | # panel hidden and locked 56 | window.run_command.assert_called_with("hide_panel", {"panel": "output.hide_errors"}) 57 | panel.set_read_only.assert_any_call(True) 58 | 59 | 60 | 61 | def test_highlight_errors_and_warnings(self): 62 | 63 | (window, view) = default_mock_window() 64 | 65 | panel = MagicMock() 66 | window.create_output_panel = Mock(return_value=panel) 67 | 68 | filePath = relative_view_file_name(view) 69 | error = create_source_error(filePath, "KindError", "") 70 | warning = create_source_error(filePath, "KindWarning", "") 71 | errors = [error, warning] 72 | 73 | Win(window).handle_source_errors(errors) 74 | 75 | # panel recreated 76 | window.create_output_panel.assert_called_with("hide_errors") 77 | window.run_command.assert_any_call("hide_panel", {"panel": "output.hide_errors"}) 78 | # panel.run_command.assert_any_call("clear_error_panel") 79 | panel.set_read_only.assert_any_call(False) 80 | 81 | # panel should have received two messages 82 | panel.run_command.assert_any_call("append_to_error_panel", {"message": "src/Main.hs:1:1: KindError:\n"}) 83 | panel.run_command.assert_any_call("append_to_error_panel", {"message": "src/Main.hs:1:1: KindWarning:\n"}) 84 | 85 | # regions added 86 | view.add_regions.assert_called_with("warnings", [ANY], "comment", "dot", sublime.DRAW_OUTLINED) 87 | view.add_regions.assert_any_call('errors', [ANY], 'invalid', 'dot', 2) 88 | 89 | # panel shown and locked 90 | window.run_command.assert_called_with("show_panel", {"panel": "output.hide_errors"}) 91 | panel.set_read_only.assert_any_call(True) 92 | 93 | def test_opens_views_for_errors(self): 94 | 95 | (window, view) = default_mock_window() 96 | window.find_open_file = Mock(side_effect=[None, view]) # first call None, second call is created 97 | 98 | panel = MagicMock() 99 | window.create_output_panel = Mock(return_value=panel) 100 | 101 | error = create_source_error("src/Lib.hs", "KindError", "") 102 | errors = [error] 103 | 104 | Win(window).handle_source_errors(errors) 105 | 106 | # should have opened the file for us. 107 | window.open_file.assert_called_with(cur_dir + "/projects/helloworld/src/Lib.hs") 108 | 109 | # panel recreated 110 | window.create_output_panel.assert_called_with("hide_errors") 111 | window.run_command.assert_any_call("hide_panel", {"panel": "output.hide_errors"}) 112 | # panel.run_command.assert_any_call("clear_error_panel") 113 | panel.set_read_only.assert_any_call(False) 114 | 115 | # panel should have received two messages 116 | panel.run_command.assert_any_call("append_to_error_panel", {"message": "src/Lib.hs:1:1: KindError:\n"}) 117 | 118 | # regions added 119 | view.add_regions.assert_called_with("warnings", [], "comment", "dot", sublime.DRAW_OUTLINED) 120 | view.add_regions.assert_any_call('errors', [ANY], 'invalid', 'dot', 2) 121 | 122 | # panel shown and locked 123 | window.run_command.assert_called_with("show_panel", {"panel": "output.hide_errors"}) 124 | panel.set_read_only.assert_any_call(True) 125 | -------------------------------------------------------------------------------- /text_commands.py: -------------------------------------------------------------------------------- 1 | try: 2 | import sublime, sublime_plugin 3 | except ImportError: 4 | from test.stubs import sublime, sublime_plugin 5 | 6 | import os, sys 7 | sys.path.append(os.path.dirname(os.path.realpath(__file__))) 8 | 9 | from utility import span_from_view_selection, first_folder, filter_enclosing 10 | from req import Req 11 | from stack_ide_manager import send_request 12 | from response import parse_span_info_response, parse_exp_types 13 | 14 | class ClearErrorPanelCommand(sublime_plugin.TextCommand): 15 | """ 16 | A clear_error_panel command to clear the error panel. 17 | """ 18 | def run(self, edit): 19 | self.view.erase(edit, sublime.Region(0, self.view.size())) 20 | 21 | class AppendToErrorPanelCommand(sublime_plugin.TextCommand): 22 | """ 23 | An append_to_error_panel command to append text to the error panel. 24 | """ 25 | def run(self, edit, message): 26 | self.view.insert(edit, self.view.size(), message + "\n\n") 27 | 28 | class ShowHsTypeAtCursorCommand(sublime_plugin.TextCommand): 29 | """ 30 | A show_hs_type_at_cursor command that requests the type of the 31 | expression under the cursor and, if available, shows it as a pop-up. 32 | """ 33 | def run(self,edit): 34 | request = Req.get_exp_types(span_from_view_selection(self.view)) 35 | send_request(self.view.window(),request, self._handle_response) 36 | 37 | def _handle_response(self,response): 38 | type_spans = list(parse_exp_types(response)) 39 | if type_spans: 40 | type_span = next(filter_enclosing(self.view, self.view.sel()[0], type_spans), None) 41 | if type_span is not None: 42 | _type, span = type_span 43 | self.view.show_popup(_type) 44 | 45 | 46 | class ShowHsInfoAtCursorCommand(sublime_plugin.TextCommand): 47 | """ 48 | A show_hs_info_at_cursor command that requests the info of the 49 | expression under the cursor and, if available, shows it as a pop-up. 50 | """ 51 | def run(self,edit): 52 | request = Req.get_exp_info(span_from_view_selection(self.view)) 53 | send_request(self.view.window(), request, self._handle_response) 54 | 55 | def _handle_response(self,response): 56 | 57 | if len(response) < 1: 58 | return 59 | 60 | infos = parse_span_info_response(response) 61 | (props, scope), span = next(infos) 62 | 63 | if not props.defSpan is None: 64 | source = "(Defined in {}:{}:{})".format(props.defSpan.filePath, props.defSpan.fromLine, props.defSpan.fromColumn) 65 | elif scope.importedFrom: 66 | source = "(Imported from {})".format(scope.importedFrom.module) 67 | 68 | self.view.show_popup("{} :: {} {}".format(props.name, 69 | props.type, 70 | source)) 71 | 72 | 73 | class GotoDefinitionAtCursorCommand(sublime_plugin.TextCommand): 74 | """ 75 | A goto_definition_at_cursor command that requests the info of the 76 | expression under the cursor and, if available, navigates to its location 77 | """ 78 | def run(self,edit): 79 | request = Req.get_exp_info(span_from_view_selection(self.view)) 80 | send_request(self.view.window(),request, self._handle_response) 81 | 82 | def _handle_response(self,response): 83 | 84 | if len(response) < 1: 85 | return 86 | 87 | infos = parse_span_info_response(response) 88 | (props, scope), span = next(infos) 89 | window = self.view.window() 90 | if props.defSpan: 91 | full_path = os.path.join(first_folder(window), props.defSpan.filePath) 92 | window.open_file( 93 | '{}:{}:{}'.format(full_path, props.defSpan.fromLine or 0, props.defSpan.fromColumn or 0), sublime.ENCODED_POSITION) 94 | elif scope.importedFrom: 95 | sublime.status_message("Cannot navigate to {}, it is imported from {}".format(props.name, scope.importedFrom.module)) 96 | else: 97 | sublime.status_message("{} not found!", props.name) 98 | 99 | class CopyHsTypeAtCursorCommand(sublime_plugin.TextCommand): 100 | """ 101 | A copy_hs_type_at_cursor command that requests the type of the 102 | expression under the cursor and, if available, puts it in the clipboard. 103 | """ 104 | def run(self,edit): 105 | request = Req.get_exp_types(span_from_view_selection(self.view)) 106 | send_request(self.view.window(), request, self._handle_response) 107 | 108 | def _handle_response(self,response): 109 | types = list(parse_exp_types(response)) 110 | if types: 111 | (type, span) = types[0] # types are ordered by relevance? 112 | sublime.set_clipboard(type) 113 | -------------------------------------------------------------------------------- /utility.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | try: 4 | import sublime 5 | except ImportError: 6 | from test.stubs import sublime 7 | 8 | import sys 9 | sys.path.append(os.path.dirname(os.path.realpath(__file__))) 10 | 11 | from log import Log 12 | 13 | complaints_shown = set() 14 | def complain(id, text): 15 | """ 16 | Show the msg as an error message (on a modal pop-up). The complaint_id is 17 | used to decide when we have already complained about something, so that 18 | we don't do it again (until reset) 19 | """ 20 | if id not in complaints_shown: 21 | complaints_shown.add(id) 22 | sublime.error_message(text) 23 | 24 | def reset_complaints(): 25 | global complaints_shown 26 | complaints_shown = set() 27 | 28 | 29 | def first_folder(window): 30 | """ 31 | We only support running one stack-ide instance per window currently, 32 | on the first folder open in that window. 33 | """ 34 | if len(window.folders()): 35 | return window.folders()[0] 36 | else: 37 | Log.normal("Couldn't find a folder for stack-ide-sublime") 38 | return None 39 | 40 | def has_cabal_file(project_path): 41 | """ 42 | Check if a cabal file exists in the project folder 43 | """ 44 | files = glob.glob(os.path.join(project_path, "*.cabal")) 45 | return len(files) > 0 46 | 47 | def expected_cabalfile(project_path): 48 | """ 49 | The cabalfile should have the same name as the directory it resides in (stack ide limitation?) 50 | """ 51 | (_, project_name) = os.path.split(project_path) 52 | return os.path.join(project_path, project_name + ".cabal") 53 | 54 | def is_stack_project(project_path): 55 | """ 56 | Determine if a stack.yaml exists in the given directory. 57 | """ 58 | return os.path.isfile(os.path.join(project_path, "stack.yaml")) 59 | 60 | def relative_view_file_name(view): 61 | """ 62 | ide-backend expects file names as relative to the cabal project root 63 | """ 64 | return view.file_name().replace(first_folder(view.window()) + os.path.sep, "") 65 | 66 | def span_from_view_selection(view): 67 | return span_from_view_region(view, view.sel()[0]) 68 | 69 | def within(smaller, larger): 70 | return smaller.begin() >= larger.begin() and smaller.end() <= larger.end() 71 | 72 | def filter_enclosing(view, region, span_pairs): 73 | return ((item, span) for item, span in span_pairs if within(region, view_region_from_span(view, span))) 74 | 75 | def format_type(raw_type): 76 | words = raw_type.replace("(", " ( ").replace(")", " ) ").replace("[", " [ ").replace("]", " ] ").replace(",", " , ").split(' ') 77 | return (" ".join(map(format_subtype, words)).replace(" ( ","(").replace(" ) ",")").replace(" [ ","[").replace(" ] ","]").replace(" , ",",")) 78 | 79 | def format_subtype(type_string): 80 | # See documentation about popups here: 81 | # http://facelessuser.github.io/sublime-markdown-popups/usage/ (official doc) 82 | # and https://www.sublimetext.com/forum/viewtopic.php?f=2&t=17583 (html support announcment) 83 | 84 | words = [x for x in type_string.split('.') if x != ''] 85 | # [x for x in type_string.split('.') is necessary to handle the `a.` part of `forall a.` properly 86 | 87 | if (len(words) > 1): 88 | s = ("_"+words[-1]) 89 | else: 90 | s = type_string 91 | 92 | if s == "->": 93 | return ('{0}'.format("->")) 94 | elif s == "(" or s == ")" or s == "[" or s == "'" or s == "]" or s=='' or s==',': 95 | return s 96 | elif (s[0] != '_' and s[0].islower()): 97 | return ('{0}'.format(s)) 98 | else: 99 | return ('{1}'.format(type_string.split(":")[-1], s)) 100 | 101 | def is_haskell_view(view): 102 | return view.match_selector(view.sel()[0].begin(), "source.haskell") 103 | 104 | def view_region_from_span(view, span): 105 | """ 106 | Maps a SourceSpan to a Region for a given view. 107 | 108 | :param sublime.View view: The view to create regions for 109 | :param SourceSpan span: The span to map to a region 110 | :rtype sublime.Region: The created Region 111 | 112 | """ 113 | return sublime.Region( 114 | view.text_point(span.fromLine - 1, span.fromColumn - 1), 115 | view.text_point(span.toLine - 1, span.toColumn - 1)) 116 | 117 | def span_from_view_region(view, region): 118 | (from_line, from_col) = view.rowcol(region.begin()) 119 | (to_line, to_col) = view.rowcol(region.end()) 120 | return { 121 | "spanFilePath": relative_view_file_name(view), 122 | "spanFromLine": from_line + 1, 123 | "spanFromColumn": to_col + 1, 124 | "spanToLine": to_line + 1, 125 | "spanToColumn": to_col + 1 126 | } 127 | -------------------------------------------------------------------------------- /watchdog.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | try: 4 | import sublime 5 | except ImportError: 6 | from test.stubs import sublime 7 | 8 | from settings import Settings 9 | from log import Log 10 | from win import Win 11 | from stack_ide_manager import StackIDEManager 12 | 13 | 14 | ############################# 15 | # Plugin development utils 16 | ############################# 17 | # Ensure existing processes are killed when we 18 | # save the plugin to prevent proliferation of 19 | # stack-ide session.13953 folders 20 | 21 | watchdog = None 22 | settings = None 23 | 24 | def plugin_loaded(): 25 | global watchdog, settings 26 | settings = load_settings() 27 | Log._set_verbosity(settings.verbosity) 28 | StackIDEManager.configure(settings) 29 | Win.show_popup = settings.show_popup 30 | Win.hoogle_url = settings.hoogle_url 31 | watchdog = StackIDEWatchdog() 32 | 33 | def plugin_unloaded(): 34 | global watchdog 35 | watchdog.kill() 36 | StackIDEManager.reset() 37 | watchdog = None 38 | 39 | 40 | def load_settings(): 41 | settings_obj = sublime.load_settings("SublimeStackIDE.sublime-settings") 42 | settings_obj.add_on_change("_on_new_settings", on_settings_changed) 43 | add_to_path = settings_obj.get('add_to_PATH', []) 44 | return Settings( 45 | settings_obj.get('verbosity', 'normal'), 46 | add_to_path if isinstance(add_to_path, list) else [], 47 | settings_obj.get('show_popup', False), 48 | settings_obj.get('hoogle_url', "http://www.stackage.org/lts/hoogle?q=") 49 | ) 50 | 51 | def on_settings_changed(): 52 | global settings 53 | updated_settings = load_settings() 54 | 55 | if updated_settings.verbosity != settings.verbosity: 56 | Log._set_verbosity(updated_settings.verbosity) 57 | elif updated_settings.add_to_PATH != settings.add_to_PATH: 58 | Log.normal("Settings changed, reloading backends") 59 | StackIDEManager.configure(updated_settings) 60 | StackIDEManager.reset() 61 | elif updated_settings.show_popup != settings.show_popup: 62 | Win.show_popup = updated_settings.show_popup 63 | elif updated_settings.hoogle_url != settings.hoogle_url: 64 | Win.hoogle_url = updated_settings.hoogle_url 65 | 66 | settings = updated_settings 67 | 68 | class StackIDEWatchdog(): 69 | """ 70 | Since I can't find any way to detect if a window closes, 71 | we use a watchdog timer to clean up stack-ide instances 72 | once we see that the window is no longer in existence. 73 | """ 74 | def __init__(self): 75 | super(StackIDEWatchdog, self).__init__() 76 | Log.normal("Starting stack-ide-sublime watchdog") 77 | self.check_for_processes() 78 | 79 | def check_for_processes(self): 80 | StackIDEManager.check_windows() 81 | self.timer = threading.Timer(1.0, self.check_for_processes) 82 | self.timer.start() 83 | 84 | def kill(self): 85 | self.timer.cancel() 86 | -------------------------------------------------------------------------------- /win.py: -------------------------------------------------------------------------------- 1 | from itertools import groupby 2 | import os 3 | 4 | try: 5 | import sublime 6 | except ImportError: 7 | from test.stubs import sublime 8 | from utility import first_folder, view_region_from_span, filter_enclosing, format_type 9 | from response import parse_source_errors, parse_exp_types 10 | import webbrowser 11 | 12 | class Win: 13 | """ 14 | Operations on Sublime windows that are relevant to us 15 | """ 16 | 17 | show_popup = False 18 | 19 | def __init__(self,window): 20 | self.window = window 21 | 22 | def update_completions(self, completions): 23 | """ 24 | Dispatches to the dummy UpdateCompletionsCommand, which is intercepted 25 | by StackIDEAutocompleteHandler's on_window_command to update its list 26 | of completions. 27 | """ 28 | self.window.run_command("update_completions", {"completions":completions}) 29 | 30 | def find_view_for_path(self, relative_path): 31 | full_path = os.path.join(first_folder(self.window), relative_path) 32 | return self.window.find_open_file(full_path) 33 | 34 | def open_view_for_path(self, relative_path): 35 | full_path = os.path.join(first_folder(self.window), relative_path) 36 | self.window.open_file(full_path) 37 | 38 | def highlight_type(self, exp_types): 39 | """ 40 | ide-backend gives us a wealth of type info for the cursor. We only use the first, 41 | most specific one for now, but it gives us the types all the way out to the topmost 42 | expression. 43 | """ 44 | type_spans = list(parse_exp_types(exp_types)) 45 | if type_spans: 46 | view = self.window.active_view() 47 | type_span = next(filter_enclosing(view, view.sel()[0], type_spans), None) 48 | if type_span is not None: 49 | (_type, span) = type_span 50 | view.set_status("type_at_cursor", _type) 51 | view.add_regions("type_at_cursor", [view_region_from_span(view, span)], "storage.type", "", sublime.DRAW_OUTLINED) 52 | if Win.show_popup: 53 | view.show_popup(format_type(_type), on_navigate= (lambda href: webbrowser.open(Win.hoogle_url + href))) 54 | return 55 | 56 | # Clear type-at-cursor display 57 | for view in self.window.views(): 58 | view.set_status("type_at_cursor", "") 59 | view.add_regions("type_at_cursor", [], "storage.type", "", sublime.DRAW_OUTLINED) 60 | 61 | 62 | def handle_source_errors(self, source_errors): 63 | """ 64 | Makes sure views containing errors are open and shows error messages + highlighting 65 | """ 66 | 67 | errors = list(parse_source_errors(source_errors)) 68 | 69 | # TODO: we should pass the errorKind too if the error has no span 70 | error_panel = self.reset_error_panel() 71 | for error in errors: 72 | error_panel.run_command("append_to_error_panel", {"message": repr(error)}) 73 | 74 | if errors: 75 | self.show_error_panel() 76 | else: 77 | self.hide_error_panel() 78 | 79 | error_panel.set_read_only(True) 80 | 81 | file_errors = list(filter(lambda error: error.span, errors)) 82 | # First, make sure we have views open for each error 83 | need_load_wait = False 84 | paths = set(error.span.filePath for error in file_errors) 85 | for path in paths: 86 | view = self.find_view_for_path(path) 87 | if not view: 88 | need_load_wait = True 89 | self.open_view_for_path(path) 90 | 91 | # If any error-holding files need to be opened, wait briefly to 92 | # make sure the file is loaded before trying to annotate it 93 | if need_load_wait: 94 | sublime.set_timeout(lambda: self.highlight_errors(file_errors), 100) 95 | else: 96 | self.highlight_errors(file_errors) 97 | 98 | 99 | def reset_error_panel(self): 100 | """ 101 | Creates and configures the error panel for the current window 102 | """ 103 | panel = self.window.create_output_panel("hide_errors") 104 | panel.set_read_only(False) 105 | 106 | # This turns on double-clickable error/warning messages in the error panel 107 | # using a regex that looks for the form file_name:line:column: error_message 108 | # The error_message could be improved as currently it says KindWarning: or KindError: 109 | # Perhaps grabbing the next line? Or the whole message? 110 | panel.settings().set("result_file_regex", "^(..[^:]*):([0-9]+):?([0-9]+)?:? (.*)$") 111 | panel.settings().set("result_base_dir", first_folder(self.window)) 112 | 113 | # Seems to force the panel to refresh after we clear it: 114 | self.hide_error_panel() 115 | 116 | # Clear the panel. TODO: should be unnecessary? https://www.sublimetext.com/forum/viewtopic.php?f=6&t=2044 117 | panel.run_command("clear_error_panel") 118 | 119 | # TODO store the panel somewhere so we can reuse it. 120 | return panel 121 | 122 | def hide_error_panel(self): 123 | self.window.run_command("hide_panel", {"panel": "output.hide_errors"}) 124 | 125 | def show_error_panel(self): 126 | self.window.run_command("show_panel", {"panel":"output.hide_errors"}) 127 | 128 | def highlight_errors(self, errors): 129 | """ 130 | Highlights the relevant regions for each error in open views 131 | """ 132 | 133 | # We gather each error by the file view it should annotate 134 | # so we can add regions in bulk to each view. 135 | error_regions_by_view_id = {} 136 | warning_regions_by_view_id = {} 137 | for path, errors_by_path in groupby(errors, lambda error: error.span.filePath): 138 | view = self.find_view_for_path(path) 139 | for kind, errors_by_kind in groupby(errors_by_path, lambda error: error.kind): 140 | if kind == 'KindWarning': 141 | warning_regions_by_view_id[view.id()] = list(view_region_from_span(view, error.span) for error in errors_by_kind) 142 | else: 143 | error_regions_by_view_id[view.id()] = list(view_region_from_span(view, error.span) for error in errors_by_kind) 144 | 145 | # Add error/warning regions to their respective views 146 | for view in self.window.views(): 147 | view.add_regions("errors", error_regions_by_view_id.get(view.id(), []), "invalid", "dot", sublime.DRAW_OUTLINED) 148 | view.add_regions("warnings", warning_regions_by_view_id.get(view.id(), []), "comment", "dot", sublime.DRAW_OUTLINED) 149 | -------------------------------------------------------------------------------- /window_commands.py: -------------------------------------------------------------------------------- 1 | try: 2 | import sublime_plugin 3 | except ImportError: 4 | from test.stubs import sublime_plugin 5 | 6 | from stack_ide_manager import StackIDEManager 7 | 8 | 9 | class SendStackIdeRequestCommand(sublime_plugin.WindowCommand): 10 | """ 11 | Allows sending commands via 12 | window.run_command("send_stack_ide_request", {"request":{"my":"request"}}) 13 | (Sublime Text uses the class name to determine the name of the command 14 | the class executes when called) 15 | """ 16 | 17 | def __init__(self, window): 18 | super(SendStackIdeRequestCommand, self).__init__(window) 19 | 20 | def run(self, request): 21 | """ 22 | Pass a request to stack-ide. 23 | Called via run_command("send_stack_ide_request", {"request":}) 24 | """ 25 | instance = StackIDEManager.for_window(self.window) 26 | if instance: 27 | instance.send_request(request) 28 | 29 | --------------------------------------------------------------------------------