Skip to content

Commit 80bc2e8

Browse files
authored
Merge pull request #42 from alanz/add-floskell-formatter
Generalize formatter plugin support, add Floskell
2 parents 9625e18 + 543d2bc commit 80bc2e8

File tree

15 files changed

+425
-170
lines changed

15 files changed

+425
-170
lines changed

.gitmodules

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,7 @@
88
# Commit git commit -m "Removed submodule <name>"
99
# Delete the now untracked submodule files
1010
# rm -rf path_to_submodule
11+
[submodule "ghcide"]
12+
path = ghcide
13+
url = https://github.com/digital-asset/ghcide.git
14+
# url = https://github.com/alanz/ghcide.git

cabal.project

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
packages:
22
./
3-
-- ghcide
3+
ghcide
44

55
tests: true
66

@@ -11,4 +11,4 @@ package ghcide
1111

1212
write-ghc-environment-files: never
1313

14-
index-state: 2020-02-04T19:45:47Z
14+
index-state: 2020-02-09T06:58:05Z

exe/Main.hs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
{-# LANGUAGE RecordWildCards #-}
55
{-# LANGUAGE ViewPatterns #-}
66
{-# LANGUAGE TupleSections #-}
7+
{-# LANGUAGE OverloadedStrings #-}
78

89
module Main(main) where
910

@@ -16,9 +17,9 @@ import Data.Default
1617
import Data.List.Extra
1718
import qualified Data.Map.Strict as Map
1819
import Data.Maybe
19-
import qualified Data.Set as Set
2020
import qualified Data.Text as T
2121
import qualified Data.Text.IO as T
22+
import Development.IDE.Core.Debouncer
2223
import Development.IDE.Core.FileStore
2324
import Development.IDE.Core.OfInterest
2425
import Development.IDE.Core.RuleTypes
@@ -36,9 +37,12 @@ import Development.IDE.Types.Options
3637
import Development.Shake (Action, action)
3738
import GHC hiding (def)
3839
import HIE.Bios
40+
import Ide.Plugin.Formatter
41+
import Ide.Plugin.Config
3942
import Language.Haskell.LSP.Messages
4043
import Language.Haskell.LSP.Types (LspId(IdInt))
4144
import Linker
45+
import qualified Data.HashSet as HashSet
4246
import System.Directory.Extra as IO
4347
import System.Exit
4448
import System.FilePath
@@ -50,6 +54,7 @@ import System.Time.Extra
5054
import Development.IDE.Plugin.CodeAction as CodeAction
5155
import Development.IDE.Plugin.Completions as Completions
5256
import Ide.Plugin.Example as Example
57+
import Ide.Plugin.Floskell as Floskell
5358
import Ide.Plugin.Ormolu as Ormolu
5459

5560
-- ---------------------------------------------------------------------
@@ -58,11 +63,12 @@ import Ide.Plugin.Ormolu as Ormolu
5863
-- server.
5964
-- These can be freely added or removed to tailor the available
6065
-- features of the server.
61-
idePlugins :: Bool -> Plugin
66+
idePlugins :: Bool -> Plugin Config
6267
idePlugins includeExample
6368
= Completions.plugin <>
6469
CodeAction.plugin <>
65-
Ormolu.plugin <>
70+
formatterPlugins [("ormolu", Ormolu.provider)
71+
,("floskell", Floskell.provider)] <>
6672
if includeExample then Example.plugin else mempty
6773

6874
-- ---------------------------------------------------------------------
@@ -91,7 +97,7 @@ main = do
9197
t <- offsetTime
9298
hPutStrLn stderr "Starting (haskell-language-server)LSP server..."
9399
hPutStrLn stderr "If you are seeing this in a terminal, you probably should have run ghcide WITHOUT the --lsp option!"
94-
runLanguageServer def (pluginHandler plugins) $ \getLspId event vfs caps -> do
100+
runLanguageServer def (pluginHandler plugins) getInitialConfig getConfigFromNotification $ \getLspId event vfs caps -> do
95101
t <- t
96102
hPutStrLn stderr $ "Started LSP server in " ++ showDuration t
97103
-- very important we only call loadSession once, and it's fast, so just do it before starting
@@ -100,7 +106,8 @@ main = do
100106
{ optReportProgress = clientSupportsProgress caps
101107
, optShakeProfiling = argsShakeProfiling
102108
}
103-
initialise caps (mainRule >> pluginRules plugins >> action kick) getLspId event (logger minBound) options vfs
109+
debouncer <- newAsyncDebouncer
110+
initialise caps (mainRule >> pluginRules plugins >> action kick) getLspId event (logger minBound) debouncer options vfs
104111
else do
105112
putStrLn $ "(haskell-language-server)Ghcide setup tester in " ++ dir ++ "."
106113
putStrLn "Report bugs at https://github.com/haskell/haskell-language-server/issues"
@@ -135,10 +142,10 @@ main = do
135142
let options =
136143
(defaultIdeOptions $ return $ return . grab)
137144
{ optShakeProfiling = argsShakeProfiling }
138-
ide <- initialise def mainRule (pure $ IdInt 0) (showEvent lock) (logger Info) options vfs
145+
ide <- initialise def mainRule (pure $ IdInt 0) (showEvent lock) (logger Info) noopDebouncer options vfs
139146

140147
putStrLn "\nStep 6/6: Type checking the files"
141-
setFilesOfInterest ide $ Set.fromList $ map toNormalizedFilePath files
148+
setFilesOfInterest ide $ HashSet.fromList $ map toNormalizedFilePath files
142149
results <- runActionSync ide $ uses TypeCheck $ map toNormalizedFilePath files
143150
let (worked, failed) = partition fst $ zip (map isJust results) files
144151
when (failed /= []) $
@@ -166,7 +173,7 @@ expandFiles = concatMapM $ \x -> do
166173
kick :: Action ()
167174
kick = do
168175
files <- getFilesOfInterest
169-
void $ uses TypeCheck $ Set.toList files
176+
void $ uses TypeCheck $ HashSet.toList files
170177

171178
-- | Print an LSP event.
172179
showEvent :: Lock -> FromServerMessage -> IO ()

ghcide

Submodule ghcide added at 286635b

haskell-language-server.cabal

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ source-repository head
2828
library
2929
exposed-modules:
3030
Ide.Cradle
31+
Ide.Plugin.Config
3132
Ide.Plugin.Example
3233
Ide.Plugin.Ormolu
34+
Ide.Plugin.Floskell
35+
Ide.Plugin.Formatter
3336
Ide.Version
3437
other-modules:
3538
Paths_haskell_language_server
@@ -39,17 +42,21 @@ library
3942
base >=4.7 && <5
4043
, aeson
4144
, binary
45+
, bytestring
4246
, Cabal
4347
, cabal-helper >= 1.0
4448
, containers
49+
, data-default
4550
, deepseq
4651
, directory
52+
, extra
4753
, filepath
54+
, floskell == 0.10.*
4855
, ghc
4956
, ghcide >= 0.1
5057
, gitrev
5158
, hashable
52-
, haskell-lsp == 0.19.*
59+
, haskell-lsp == 0.20.*
5360
, hie-bios >= 0.4
5461
, hslogger
5562
, optparse-simple
@@ -117,6 +124,7 @@ executable haskell-language-server
117124
, optparse-applicative
118125
, shake >= 0.17.5
119126
, text
127+
, unordered-containers
120128
default-language: Haskell2010
121129

122130
executable haskell-language-server-wrapper

src/Ide/Plugin/Config.hs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
{-# LANGUAGE RecordWildCards #-}
2+
{-# LANGUAGE DeriveGeneric #-}
3+
{-# LANGUAGE FlexibleInstances #-}
4+
{-# LANGUAGE OverloadedStrings #-}
5+
{-# LANGUAGE TypeFamilies #-}
6+
module Ide.Plugin.Config
7+
(
8+
getInitialConfig
9+
, getConfigFromNotification
10+
, Config(..)
11+
) where
12+
13+
import qualified Data.Aeson as A
14+
import Data.Aeson hiding ( Error )
15+
import Data.Default
16+
import qualified Data.Text as T
17+
import Language.Haskell.LSP.Types
18+
19+
-- ---------------------------------------------------------------------
20+
21+
-- | Given a DidChangeConfigurationNotification message, this function returns the parsed
22+
-- Config object if possible.
23+
getConfigFromNotification :: DidChangeConfigurationNotification -> Either T.Text Config
24+
getConfigFromNotification (NotificationMessage _ _ (DidChangeConfigurationParams p)) =
25+
case fromJSON p of
26+
A.Success c -> Right c
27+
A.Error err -> Left $ T.pack err
28+
29+
-- | Given an InitializeRequest message, this function returns the parsed
30+
-- Config object if possible. Otherwise, it returns the default configuration
31+
getInitialConfig :: InitializeRequest -> Either T.Text Config
32+
getInitialConfig (RequestMessage _ _ _ InitializeParams{_initializationOptions = Nothing }) = Right def
33+
getInitialConfig (RequestMessage _ _ _ InitializeParams{_initializationOptions = Just opts}) =
34+
case fromJSON opts of
35+
A.Success c -> Right c
36+
A.Error err -> Left $ T.pack err
37+
38+
-- ---------------------------------------------------------------------
39+
40+
-- | We (initially anyway) mirror the hie configuration, so that existing
41+
-- clients can simply switch executable and not have any nasty surprises. There
42+
-- will be surprises relating to config options being ignored, initially though.
43+
data Config =
44+
Config
45+
{ hlintOn :: Bool
46+
, diagnosticsOnChange :: Bool
47+
, maxNumberOfProblems :: Int
48+
, diagnosticsDebounceDuration :: Int
49+
, liquidOn :: Bool
50+
, completionSnippetsOn :: Bool
51+
, formatOnImportOn :: Bool
52+
, formattingProvider :: T.Text
53+
} deriving (Show,Eq)
54+
55+
instance Default Config where
56+
def = Config
57+
{ hlintOn = True
58+
, diagnosticsOnChange = True
59+
, maxNumberOfProblems = 100
60+
, diagnosticsDebounceDuration = 350000
61+
, liquidOn = False
62+
, completionSnippetsOn = True
63+
, formatOnImportOn = True
64+
-- , formattingProvider = "brittany"
65+
, formattingProvider = "ormolu"
66+
-- , formattingProvider = "floskell"
67+
}
68+
69+
-- TODO: Add API for plugins to expose their own LSP config options
70+
instance A.FromJSON Config where
71+
parseJSON = A.withObject "Config" $ \v -> do
72+
s <- v .: "languageServerHaskell"
73+
flip (A.withObject "Config.settings") s $ \o -> Config
74+
<$> o .:? "hlintOn" .!= hlintOn def
75+
<*> o .:? "diagnosticsOnChange" .!= diagnosticsOnChange def
76+
<*> o .:? "maxNumberOfProblems" .!= maxNumberOfProblems def
77+
<*> o .:? "diagnosticsDebounceDuration" .!= diagnosticsDebounceDuration def
78+
<*> o .:? "liquidOn" .!= liquidOn def
79+
<*> o .:? "completionSnippetsOn" .!= completionSnippetsOn def
80+
<*> o .:? "formatOnImportOn" .!= formatOnImportOn def
81+
<*> o .:? "formattingProvider" .!= formattingProvider def
82+
83+
-- 2017-10-09 23:22:00.710515298 [ThreadId 11] - ---> {"jsonrpc":"2.0","method":"workspace/didChangeConfiguration","params":{"settings":{"languageServerHaskell":{"maxNumberOfProblems":100,"hlintOn":true}}}}
84+
-- 2017-10-09 23:22:00.710667381 [ThreadId 15] - reactor:got didChangeConfiguration notification:
85+
-- NotificationMessage
86+
-- {_jsonrpc = "2.0"
87+
-- , _method = WorkspaceDidChangeConfiguration
88+
-- , _params = DidChangeConfigurationParams
89+
-- {_settings = Object (fromList [("languageServerHaskell",Object (fromList [("hlintOn",Bool True)
90+
-- ,("maxNumberOfProblems",Number 100.0)]))])}}
91+
92+
instance A.ToJSON Config where
93+
toJSON (Config h diag m d l c f fp) = object [ "languageServerHaskell" .= r ]
94+
where
95+
r = object [ "hlintOn" .= h
96+
, "diagnosticsOnChange" .= diag
97+
, "maxNumberOfProblems" .= m
98+
, "diagnosticsDebounceDuration" .= d
99+
, "liquidOn" .= l
100+
, "completionSnippetsOn" .= c
101+
, "formatOnImportOn" .= f
102+
, "formattingProvider" .= fp
103+
]

src/Ide/Plugin/Example.hs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import Data.Binary
2020
import Data.Functor
2121
import qualified Data.HashMap.Strict as Map
2222
import Data.Hashable
23-
import qualified Data.Set as Set
23+
import qualified Data.HashSet as HashSet
2424
import qualified Data.Text as T
2525
import Data.Typeable
2626
import Development.IDE.Core.OfInterest
@@ -42,7 +42,7 @@ import Text.Regex.TDFA.Text()
4242

4343
-- ---------------------------------------------------------------------
4444

45-
plugin :: Plugin
45+
plugin :: Plugin c
4646
plugin = Plugin exampleRules handlersExample
4747
<> codeActionPlugin codeAction
4848
<> Plugin mempty handlersCodeLens
@@ -54,7 +54,7 @@ blah :: NormalizedFilePath -> Position -> Action (Maybe (Maybe Range, [T.Text]))
5454
blah _ (Position line col)
5555
= return $ Just (Just (Range (Position line col) (Position (line+1) 0)), ["example hover"])
5656

57-
handlersExample :: PartialHandlers
57+
handlersExample :: PartialHandlers c
5858
handlersExample = PartialHandlers $ \WithMessage{..} x ->
5959
return x{LSP.hoverHandler = withResponse RspHover $ const hover}
6060

@@ -78,7 +78,7 @@ exampleRules = do
7878

7979
action $ do
8080
files <- getFilesOfInterest
81-
void $ uses Example $ Set.toList files
81+
void $ uses Example $ HashSet.toList files
8282

8383
mkDiag :: NormalizedFilePath
8484
-> DiagnosticSource
@@ -100,7 +100,7 @@ mkDiag file diagSource sev loc msg = (file, D.ShowDiag,)
100100

101101
-- | Generate code actions.
102102
codeAction
103-
:: LSP.LspFuncs ()
103+
:: LSP.LspFuncs c
104104
-> IdeState
105105
-> TextDocumentIdentifier
106106
-> Range
@@ -118,14 +118,14 @@ codeAction _lsp _state (TextDocumentIdentifier uri) _range CodeActionContext{_di
118118
-- ---------------------------------------------------------------------
119119

120120
-- | Generate code lenses.
121-
handlersCodeLens :: PartialHandlers
121+
handlersCodeLens :: PartialHandlers c
122122
handlersCodeLens = PartialHandlers $ \WithMessage{..} x -> return x{
123123
LSP.codeLensHandler = withResponse RspCodeLens codeLens,
124124
LSP.executeCommandHandler = withResponseAndRequest RspExecuteCommand ReqApplyWorkspaceEdit executeAddSignatureCommand
125125
}
126126

127127
codeLens
128-
:: LSP.LspFuncs ()
128+
:: LSP.LspFuncs c
129129
-> IdeState
130130
-> CodeLensParams
131131
-> IO (Either ResponseError (List CodeLens))
@@ -149,7 +149,7 @@ codeLens _lsp ideState CodeLensParams{_textDocument=TextDocumentIdentifier uri}
149149

150150
-- | Execute the "codelens.todo" command.
151151
executeAddSignatureCommand
152-
:: LSP.LspFuncs ()
152+
:: LSP.LspFuncs c
153153
-> IdeState
154154
-> ExecuteCommandParams
155155
-> IO (Value, Maybe (ServerMethod, ApplyWorkspaceEditParams))

src/Ide/Plugin/Floskell.hs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{-# LANGUAGE RecordWildCards #-}
2+
{-# LANGUAGE CPP #-}
3+
{-# LANGUAGE OverloadedStrings #-}
4+
{-# LANGUAGE ScopedTypeVariables #-}
5+
{-# LANGUAGE TypeApplications #-}
6+
{-# LANGUAGE ViewPatterns #-}
7+
8+
module Ide.Plugin.Floskell
9+
(
10+
provider
11+
)
12+
where
13+
14+
import qualified Data.ByteString.Lazy as BS
15+
import qualified Data.Text as T
16+
import qualified Data.Text.Encoding as T
17+
import Development.IDE.Types.Diagnostics as D
18+
import Development.IDE.Types.Location
19+
import Floskell
20+
import Ide.Plugin.Formatter
21+
import Language.Haskell.LSP.Types
22+
import Text.Regex.TDFA.Text()
23+
24+
-- ---------------------------------------------------------------------
25+
26+
-- | Format provider of Floskell.
27+
-- Formats the given source in either a given Range or the whole Document.
28+
-- If the provider fails an error is returned that can be displayed to the user.
29+
provider :: FormattingProvider IO
30+
provider _ideState typ contents fp _ = do
31+
let file = fromNormalizedFilePath fp
32+
config <- findConfigOrDefault file
33+
let (range, selectedContents) = case typ of
34+
FormatText -> (fullRange contents, contents)
35+
FormatRange r -> (r, extractRange r contents)
36+
result = reformat config (Just file) (BS.fromStrict (T.encodeUtf8 selectedContents))
37+
case result of
38+
Left err -> return $ Left $ responseError (T.pack $ "floskellCmd: " ++ err)
39+
Right new -> return $ Right $ List [TextEdit range (T.decodeUtf8 (BS.toStrict new))]
40+
41+
-- | Find Floskell Config, user and system wide or provides a default style.
42+
-- Every directory of the filepath will be searched to find a user configuration.
43+
-- Also looks into places such as XDG_CONFIG_DIRECTORY<https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html>.
44+
-- This function may not throw an exception and returns a default config.
45+
findConfigOrDefault :: FilePath -> IO AppConfig
46+
findConfigOrDefault file = do
47+
mbConf <- findAppConfigIn file
48+
case mbConf of
49+
Just confFile -> readAppConfig confFile
50+
Nothing ->
51+
let gibiansky = head (filter (\s -> styleName s == "gibiansky") styles)
52+
in return $ defaultAppConfig { appStyle = gibiansky }
53+
54+
-- ---------------------------------------------------------------------

0 commit comments

Comments
 (0)