├── .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 | 
3 | [](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 | 
32 | 
33 | 
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 |
--------------------------------------------------------------------------------