Skip to content

Hlint plugin #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@
# Commit git commit -m "Removed submodule <name>"
# Delete the now untracked submodule files
# rm -rf path_to_submodule
[submodule "ghcide"]
path = ghcide
# url = https://github.com/digital-asset/ghcide.git
url = https://github.com/alanz/ghcide.git
4 changes: 2 additions & 2 deletions cabal.project
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
packages:
./
-- ghcide
ghcide

tests: true

Expand All @@ -11,4 +11,4 @@ package ghcide

write-ghc-environment-files: never

index-state: 2020-02-04T19:45:47Z
index-state: 2020-02-09T06:58:05Z
19 changes: 14 additions & 5 deletions exe/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE OverloadedStrings #-}

module Main(main) where

Expand All @@ -19,6 +20,7 @@ import Data.Maybe
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Development.IDE.Core.Debouncer
import Development.IDE.Core.FileStore
import Development.IDE.Core.OfInterest
import Development.IDE.Core.RuleTypes
Expand All @@ -36,6 +38,8 @@ import Development.IDE.Types.Options
import Development.Shake (Action, action)
import GHC hiding (def)
import HIE.Bios
import Ide.Plugin.Formatter
import Ide.Plugin.Config
import Language.Haskell.LSP.Messages
import Language.Haskell.LSP.Types (LspId(IdInt))
import Linker
Expand All @@ -50,6 +54,8 @@ import System.Time.Extra
import Development.IDE.Plugin.CodeAction as CodeAction
import Development.IDE.Plugin.Completions as Completions
import Ide.Plugin.Example as Example
import Ide.Plugin.Floskell as Floskell
import Ide.Plugin.Hlint as Hlint
import Ide.Plugin.Ormolu as Ormolu

-- ---------------------------------------------------------------------
Expand All @@ -58,11 +64,13 @@ import Ide.Plugin.Ormolu as Ormolu
-- server.
-- These can be freely added or removed to tailor the available
-- features of the server.
idePlugins :: Bool -> Plugin
idePlugins :: Bool -> Plugin Config
idePlugins includeExample
= Completions.plugin <>
CodeAction.plugin <>
Ormolu.plugin <>
formatterPlugins [("ormolu", Ormolu.provider)
,("floskell", Floskell.provider)] <>
Hlint.plugin <>
if includeExample then Example.plugin else mempty

-- ---------------------------------------------------------------------
Expand Down Expand Up @@ -91,7 +99,7 @@ main = do
t <- offsetTime
hPutStrLn stderr "Starting (haskell-language-server)LSP server..."
hPutStrLn stderr "If you are seeing this in a terminal, you probably should have run ghcide WITHOUT the --lsp option!"
runLanguageServer def (pluginHandler plugins) $ \getLspId event vfs caps -> do
runLanguageServer def (pluginHandler plugins) getInitialConfig getConfigFromNotification $ \getLspId event vfs caps -> do
t <- t
hPutStrLn stderr $ "Started LSP server in " ++ showDuration t
-- very important we only call loadSession once, and it's fast, so just do it before starting
Expand All @@ -100,7 +108,8 @@ main = do
{ optReportProgress = clientSupportsProgress caps
, optShakeProfiling = argsShakeProfiling
}
initialise caps (mainRule >> pluginRules plugins >> action kick) getLspId event (logger minBound) options vfs
debouncer <- newAsyncDebouncer
initialise caps (mainRule >> pluginRules plugins >> action kick) getLspId event (logger minBound) debouncer options vfs
else do
putStrLn $ "(haskell-language-server)Ghcide setup tester in " ++ dir ++ "."
putStrLn "Report bugs at https://github.com/haskell/haskell-language-server/issues"
Expand Down Expand Up @@ -135,7 +144,7 @@ main = do
let options =
(defaultIdeOptions $ return $ return . grab)
{ optShakeProfiling = argsShakeProfiling }
ide <- initialise def mainRule (pure $ IdInt 0) (showEvent lock) (logger Info) options vfs
ide <- initialise def mainRule (pure $ IdInt 0) (showEvent lock) (logger Info) noopDebouncer options vfs

putStrLn "\nStep 6/6: Type checking the files"
setFilesOfInterest ide $ Set.fromList $ map toNormalizedFilePath files
Expand Down
1 change: 1 addition & 0 deletions ghcide
Submodule ghcide added at 24116b
12 changes: 11 additions & 1 deletion haskell-language-server.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ source-repository head
library
exposed-modules:
Ide.Cradle
Ide.Plugin.Config
Ide.Plugin.Example
Ide.Plugin.Hlint
Ide.Plugin.Ormolu
Ide.Plugin.Floskell
Ide.Plugin.Formatter
Ide.Version
other-modules:
Paths_haskell_language_server
Expand All @@ -39,18 +43,24 @@ library
base >=4.7 && <5
, aeson
, binary
, bytestring
, Cabal
, cabal-helper >= 1.0
, containers
, data-default
, deepseq
, directory
, extra
, filepath
, floskell == 0.10.*
, ghc
, ghcide >= 0.1
, gitrev
, hashable
, haskell-lsp == 0.19.*
, haskell-lsp == 0.20.*
, haskell-src-exts
, hie-bios >= 0.4
, hlint >= 2.2.10
, hslogger
, optparse-simple
, process
Expand Down
102 changes: 102 additions & 0 deletions src/Ide/Plugin/Config.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
module Ide.Plugin.Config
(
getInitialConfig
, getConfigFromNotification
, Config(..)
) where

import qualified Data.Aeson as A
import Data.Aeson hiding ( Error )
import Data.Default
import qualified Data.Text as T
import Language.Haskell.LSP.Types

-- ---------------------------------------------------------------------

-- | Given a DidChangeConfigurationNotification message, this function returns the parsed
-- Config object if possible.
getConfigFromNotification :: DidChangeConfigurationNotification -> Either T.Text Config
getConfigFromNotification (NotificationMessage _ _ (DidChangeConfigurationParams p)) =
case fromJSON p of
A.Success c -> Right c
A.Error err -> Left $ T.pack err

-- | Given an InitializeRequest message, this function returns the parsed
-- Config object if possible. Otherwise, it returns the default configuration
getInitialConfig :: InitializeRequest -> Either T.Text Config
getInitialConfig (RequestMessage _ _ _ InitializeParams{_initializationOptions = Nothing }) = Right def
getInitialConfig (RequestMessage _ _ _ InitializeParams{_initializationOptions = Just opts}) =
case fromJSON opts of
A.Success c -> Right c
A.Error err -> Left $ T.pack err

-- ---------------------------------------------------------------------

-- | We (initially anyway) mirror the hie configuration, so that existing
-- clients can simply switch executable and not have any nasty surprises. There
-- will be surprises relating to config options being ignored, initially though.
data Config =
Config
{ hlintOn :: Bool
, diagnosticsOnChange :: Bool
, maxNumberOfProblems :: Int
, diagnosticsDebounceDuration :: Int
, liquidOn :: Bool
, completionSnippetsOn :: Bool
, formatOnImportOn :: Bool
, formattingProvider :: T.Text
} deriving (Show,Eq)

instance Default Config where
def = Config
{ hlintOn = True
, diagnosticsOnChange = True
, maxNumberOfProblems = 100
, diagnosticsDebounceDuration = 350000
, liquidOn = False
, completionSnippetsOn = True
, formatOnImportOn = True
-- , formattingProvider = "brittany"
, formattingProvider = "ormolu"
}

-- TODO: Add API for plugins to expose their own LSP config options
instance A.FromJSON Config where
parseJSON = A.withObject "Config" $ \v -> do
s <- v .: "languageServerHaskell"
flip (A.withObject "Config.settings") s $ \o -> Config
<$> o .:? "hlintOn" .!= hlintOn def
<*> o .:? "diagnosticsOnChange" .!= diagnosticsOnChange def
<*> o .:? "maxNumberOfProblems" .!= maxNumberOfProblems def
<*> o .:? "diagnosticsDebounceDuration" .!= diagnosticsDebounceDuration def
<*> o .:? "liquidOn" .!= liquidOn def
<*> o .:? "completionSnippetsOn" .!= completionSnippetsOn def
<*> o .:? "formatOnImportOn" .!= formatOnImportOn def
<*> o .:? "formattingProvider" .!= formattingProvider def

-- 2017-10-09 23:22:00.710515298 [ThreadId 11] - ---> {"jsonrpc":"2.0","method":"workspace/didChangeConfiguration","params":{"settings":{"languageServerHaskell":{"maxNumberOfProblems":100,"hlintOn":true}}}}
-- 2017-10-09 23:22:00.710667381 [ThreadId 15] - reactor:got didChangeConfiguration notification:
-- NotificationMessage
-- {_jsonrpc = "2.0"
-- , _method = WorkspaceDidChangeConfiguration
-- , _params = DidChangeConfigurationParams
-- {_settings = Object (fromList [("languageServerHaskell",Object (fromList [("hlintOn",Bool True)
-- ,("maxNumberOfProblems",Number 100.0)]))])}}

instance A.ToJSON Config where
toJSON (Config h diag m d l c f fp) = object [ "languageServerHaskell" .= r ]
where
r = object [ "hlintOn" .= h
, "diagnosticsOnChange" .= diag
, "maxNumberOfProblems" .= m
, "diagnosticsDebounceDuration" .= d
, "liquidOn" .= l
, "completionSnippetsOn" .= c
, "formatOnImportOn" .= f
, "formattingProvider" .= fp
]
12 changes: 6 additions & 6 deletions src/Ide/Plugin/Example.hs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import Text.Regex.TDFA.Text()

-- ---------------------------------------------------------------------

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

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

Expand Down Expand Up @@ -100,7 +100,7 @@ mkDiag file diagSource sev loc msg = (file, D.ShowDiag,)

-- | Generate code actions.
codeAction
:: LSP.LspFuncs ()
:: LSP.LspFuncs c
-> IdeState
-> TextDocumentIdentifier
-> Range
Expand All @@ -118,14 +118,14 @@ codeAction _lsp _state (TextDocumentIdentifier uri) _range CodeActionContext{_di
-- ---------------------------------------------------------------------

-- | Generate code lenses.
handlersCodeLens :: PartialHandlers
handlersCodeLens :: PartialHandlers c
handlersCodeLens = PartialHandlers $ \WithMessage{..} x -> return x{
LSP.codeLensHandler = withResponse RspCodeLens codeLens,
LSP.executeCommandHandler = withResponseAndRequest RspExecuteCommand ReqApplyWorkspaceEdit executeAddSignatureCommand
}

codeLens
:: LSP.LspFuncs ()
:: LSP.LspFuncs c
-> IdeState
-> CodeLensParams
-> IO (Either ResponseError (List CodeLens))
Expand All @@ -149,7 +149,7 @@ codeLens _lsp ideState CodeLensParams{_textDocument=TextDocumentIdentifier uri}

-- | Execute the "codelens.todo" command.
executeAddSignatureCommand
:: LSP.LspFuncs ()
:: LSP.LspFuncs c
-> IdeState
-> ExecuteCommandParams
-> IO (Value, Maybe (ServerMethod, ApplyWorkspaceEditParams))
Expand Down
54 changes: 54 additions & 0 deletions src/Ide/Plugin/Floskell.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ViewPatterns #-}

module Ide.Plugin.Floskell
(
provider
)
where

import qualified Data.ByteString.Lazy as BS
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Development.IDE.Types.Diagnostics as D
import Development.IDE.Types.Location
import Floskell
import Ide.Plugin.Formatter
import Language.Haskell.LSP.Types
import Text.Regex.TDFA.Text()

-- ---------------------------------------------------------------------

-- | Format provider of Floskell.
-- Formats the given source in either a given Range or the whole Document.
-- If the provider fails an error is returned that can be displayed to the user.
provider :: FormattingProvider IO
provider _ideState typ contents fp _ = do
let file = fromNormalizedFilePath fp
config <- findConfigOrDefault file
let (range, selectedContents) = case typ of
FormatText -> (fullRange contents, contents)
FormatRange r -> (r, extractRange r contents)
result = reformat config (Just file) (BS.fromStrict (T.encodeUtf8 selectedContents))
case result of
Left err -> return $ Left $ responseError (T.pack $ "floskellCmd: " ++ err)
Right new -> return $ Right $ List [TextEdit range (T.decodeUtf8 (BS.toStrict new))]

-- | Find Floskell Config, user and system wide or provides a default style.
-- Every directory of the filepath will be searched to find a user configuration.
-- Also looks into places such as XDG_CONFIG_DIRECTORY<https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html>.
-- This function may not throw an exception and returns a default config.
findConfigOrDefault :: FilePath -> IO AppConfig
findConfigOrDefault file = do
mbConf <- findAppConfigIn file
case mbConf of
Just confFile -> readAppConfig confFile
Nothing ->
let gibiansky = head (filter (\s -> styleName s == "gibiansky") styles)
in return $ defaultAppConfig { appStyle = gibiansky }

-- ---------------------------------------------------------------------
Loading