packages feed

fpco-api (empty) → 1.0.0

raw patch · 14 files changed

+2366/−0 lines, 14 filesdep +ConfigFiledep +aesondep +attoparsecsetup-changed

Dependencies added: ConfigFile, aeson, attoparsec, base, bytestring, containers, data-default, directory, failure, fay, filepath, fpco-api, ghc-prim, http-conduit, http-types, lifted-base, monad-extras, monad-logger, mtl, network, optparse-applicative, persistent, persistent-template, pretty-show, process, random, resourcet, safe, syb, template-haskell, text, texts, unordered-containers, vector, yesod-fay

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2013, FP Complete++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of FP Complete nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ fpco-api.cabal view
@@ -0,0 +1,79 @@+name:                 fpco-api+version:              1.0.0+synopsis:             API interface for the FP Complete API.+description:          Data types and serialization for communicating with the FP Complete IDE API.+homepage:             https://www.fpcomplete.com/page/api+license:              BSD3+license-file:         LICENSE+author:               FP Complete+maintainer:           dev@fpcomplete.com+copyright:            2013 FP Complete+category:             Development+build-type:           Simple+cabal-version:        >=1.10++flag jenkins-build+  default: False++library+  ghc-options:       -Wall -O2 -threaded+  hs-source-dirs:     src/library+  exposed-modules:    FP.API, FP.API.Run, FP.API.Types, FP.Server, FP.Server.Types, FP.Server.Config,+                      FP.Server.Spans, FP.API.Convert+  other-modules:      FP.API.TH, FFI+  default-language:   Haskell2010+  build-depends:      base >=4 && < 5,+                      aeson ==0.6.*,+                      bytestring ==0.9.*,+                      ConfigFile ==1.1.*,+                      data-default ==0.5.*,+                      text ==0.11.*,+                      network ==2.4.*,+                      optparse-applicative ==0.5.*,+                      safe ==0.3.*,+                      directory ==1.1.*,+                      filepath ==1.3.*,+                      fay ==0.18.*,+                      lifted-base ==0.2.*,+                      monad-extras ==0.5.*,+                      monad-logger ==0.3.*,+                      mtl ==2.1.*,+                      containers ==0.4.*,+                      http-conduit ==1.9.*,+                      texts ==0.3.*,+                      ghc-prim ==0.2.*,+                      template-haskell ==2.7.*,+                      attoparsec ==0.10.*,+                      syb ==0.4.*,+                      unordered-containers ==0.2.*,+                      vector ==0.10.*,+                      pretty-show ==1.6.*,+                      failure ==0.2.*,+                      resourcet ==0.4.*,+                      yesod-fay ==0.4.*,+                      http-types ==0.8.*,+                      persistent ==1.2.*,+                      persistent-template ==1.2.*,+                      random ==1.0.*++executable fpco-api+  if flag(jenkins-build)+    buildable: False+  ghc-options:        -Wall -threaded -O2+  main-is:            Main.hs+  default-extensions: CPP+  hs-source-dirs:     src/executables+  default-language:   Haskell2010+  build-depends:      base >=4 && < 5,+                      fpco-api,+                      optparse-applicative == 0.5.*,+                      bytestring == 0.9.*,+                      network ==2.4.*,+                      safe ==0.3.*,+                      aeson ==0.6.*,+                      text ==0.11.*,+                      directory ==1.1.*,+                      filepath ==1.3.*,+                      ConfigFile ==1.1.*,+                      data-default ==0.5.*,+                      process == 1.1.*
+ src/executables/Main.hs view
@@ -0,0 +1,306 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE NoMonomorphismRestriction #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Main fpco-api executable.++module Main where++import           FP.API.Types+import           FP.Server+import           FP.Server.Types+import           FP.Server.Spans++++import           Control.Exception+import           Control.Monad+import           Data.Aeson+import qualified Data.ByteString.Lazy as L+import           Data.ConfigFile+import           Data.Default+import           Data.List+import           Data.Maybe+import           Data.Monoid+import           Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.IO as T+import           Network+import           Options.Applicative+import           Prelude hiding (catch)+import           Safe+import           System.Directory+import           System.Environment+import           System.FilePath+import           System.IO++--------------------------------------------------------------------------------+-- Main++-- | Main program entry point.+main :: IO ()+main = do+  join (execParser opts)++   where opts =+           info (helper <*> config)+                (mconcat [fullDesc+                         ,header "fpco-api"+                         ,progDesc "A program for easily talking to the FP Complete API"])+         config =+           subparser+             (mconcat [cmd startCommand "start" "Start a session server"+                      ,cmd configureCommand "config" "Generate a configuration"+                      ,cmd checkCommand "check" "Type check the given file"+                      ,cmd downloadCommand "download" "Download a project and write Emacs config"+                      ,cmd emacsConfigCommand "emacs-config" "Write Emacs config"])+         cmd run name desc = command name (info run (progDesc desc))++--------------------------------------------------------------------------------+-- Commands++-- | Start the server.+startCommand :: Parser (IO ())+startCommand =+  pure (\agent -> withConfig agent (runWithConfig (startServer False))) <*>+  agentOpt <*>+  configFileOpt++-- | Make a configuration.+configureCommand :: Parser (IO ())+configureCommand =+  pure configure <*>+  configFileOpt++-- | Check the given module.+checkCommand :: Parser (IO ())+checkCommand =+  pure (\pid root name path ->+          withConfig Nothing (check pid root name path)) <*>+  fayPidOpt <*>+  rootOpt <*>+  filepathOpt <*>+  tmppathOpt <*>+  configFileOpt++  where check pid root name path config =+          sendCommand config root (MsgCheckModule pid root name path)++-- | Download the given module.+downloadCommand :: Parser (IO ())+downloadCommand =+  pure (\pid -> withConfig Nothing (\c -> download pid c { configStartServer = True })) <*>+  fayProjectOpt <*>+  configFileOpt++ where download pid config = do+         root <- getCurrentDirectory+         putStrLn "Downloading project files ..."+         sendCommand config root (MsgDownloadFiles pid root)+         putStrLn "Downloaded project files."+         sendCommand config root (MsgWriteEmacsConfig pid root)+         putStrLn "Wrote Emacs configuration."++-- | EmacsConfig the given module.+emacsConfigCommand :: Parser (IO ())+emacsConfigCommand =+  pure (\pid -> withConfig Nothing (\c -> emacsConfig pid c { configStartServer = True })) <*>+  fayProjectOpt <*>+  configFileOpt++ where emacsConfig pid config = do+         root <- getCurrentDirectory+         sendCommand config root (MsgWriteEmacsConfig pid root)+         putStrLn "Wrote Emacs configuration."++--------------------------------------------------------------------------------+-- Options++-- | -agent option.+agentOpt :: Parser (Maybe Text)+agentOpt =+  fmap (fmap T.pack)+       (optional (strOption (metavar "AGENT" <> long "agent" <> short 'a' <>+                             help "User agent (IDE of choice)")))++-- | -config option.+configFileOpt :: Parser (Maybe String)+configFileOpt =+  optional (strOption (metavar "CONFIG" <> long "config" <> short 'c' <>+                       help "Config file"))++-- | PID option.+fayPidOpt :: Parser FayProjectId+fayPidOpt =+  argument (return . toFay)+   (metavar "PID")++-- | PROJECT option.+fayProjectOpt :: Parser (Either Text FayProjectId)+fayProjectOpt =+  argument (return . toFayPid)+           (metavar "PROJECT" <>+            help "Either a project URL or a project ID")++-- | ROOT option.+rootOpt :: Parser FilePath+rootOpt =+  argument return+           (metavar "ROOT" <>+            help "Project root directory")++-- | FILEPATH option.+filepathOpt :: Parser FilePath+filepathOpt =+  argument return+           (metavar "FILEPATH" <>+            help "The file path")++-- | TMPPATH option.+tmppathOpt :: Parser FilePath+tmppathOpt =+  argument return+           (metavar "TMPPATH" <>+            help "The temporary file path containing the actual contents of the module")++--------------------------------------------------------------------------------+-- Project ID++-- | Convert to project ID.+toFay :: String -> FayProjectId+toFay i = FayProjectId (T.pack (show (read i :: Int)))++-- | Convert to a project ID or project URL.+toFayPid :: String -> Either Text FayProjectId+toFayPid i+  | isPrefixOf "http" i = Left (T.pack i)+  | otherwise = Right (toFay i)++--------------------------------------------------------------------------------+-- Client++-- | Send a command to the fpco-server and output the response to stdout.+sendCommand :: ToJSON a => Config -> FilePath -> a -> IO ()+sendCommand config root msg = do+  env <- getEnvironment+  let host = fromMaybe "localhost" (lookup "FPCO_HOST" env)+      port = fromMaybe (configPort config) (lookup "FPCO_PORT" env >>= readMay)+  h <- connectOrStartServer config host port+  hSetBuffering h NoBuffering+  L.hPutStr h (encode msg)+  hPutStrLn h ""+  bytes <- L.hGetContents h+  case decode bytes of+    Just (ReplyCompileInfos infos) ->+      mapM_ (T.putStrLn . printSourceInfo root)+            infos+    Just (ReplyOK ()) ->+      return ()+    _ ->+      if L.null bytes+         then error ("Connection to listener was closed.")+         else error ("Unexpected reply from fpco-server: " +++                     show bytes)++-- | Connect to the local server, if it can't connect, start the+-- server temporarily.+connectOrStartServer :: Config -> HostName -> Integer -> IO Handle+connectOrStartServer config host port = do+  catch (connect False)+        (\(e :: IOException) ->+           if configStartServer config+              then do putStrLn "Couldn't connect to local listener (probably not started yet). Running a temporary server instead."+                      runWithConfig (startServer True) config { configDebug = False }+                      connect True+              else throw e)++  where connect mention =+          do h <- connectTo host (PortNumber (fromInteger port))+             when mention+                  (putStrLn "Connected to temporary local listener ...")+             return h++-- | Print a flycheck-readable error/warning/hint.+printSourceInfo :: FilePath -> SourceInfo -> Text+printSourceInfo root (SourceInfo _ thespan msg) =+  printedSpan <> ":\n   " <>+  T.unlines (map pad (T.lines msg)) <> "\n"++  where pad x | T.isPrefixOf " " x = x+              | otherwise = " " <> x+        printedSpan =+          case thespan of+            TextSpan span' -> "<" <> T.pack span' <> ">"+            ProperSpan span' -> printSourceSpan root span'++--------------------------------------------------------------------------------+-- Configuration++-- | Make a configuration. Optionally a path to write to can be+-- passed.+configure :: Maybe FilePath -> IO ()+configure mfp = do+  fp <- getConfigPath mfp+  hSetBuffering stdout NoBuffering+  token <- prompt "your token"+  writeConfig fp def { configToken = token }+  putStrLn ("Wrote configuration to " ++ fp)+  return ()++  where prompt p = do putStr ("Please enter " ++ p ++ ": ")+                      getLine++-- | Write out a configuration.+writeConfig :: FilePath -> Config -> IO ()+writeConfig fp config =+  either (error . show)+         (writeFile fp)+         cp++  where cp = fmap (to_string :: ConfigParser -> String)+                  (write emptyCP)+        write = addSection "API" >=>+                addSection "SERVER" >=>+                setValue "API" "token" (configToken config) >=>+                setValue "API" "url" (configUrl config) >=>+                setValue "SERVER" "port" (show (configPort config))+        addSection = flip add_section+        setValue spec key v c = set c spec key v++-- | Run a command with the configuration, if there is one and make+-- one if there is not.+withConfig :: Maybe Text -> (Config -> IO a) -> Maybe FilePath -> IO a+withConfig agent cont mfp = go where+  go = do+    fp <- getConfigPath mfp+    exists <- doesFileExist fp+    if exists+       then do contents <- readFile fp+               case readConfig agent contents of+                 Left cperr -> error cperr+                 Right config -> cont config+       else do putStrLn ("No configuration found at " ++ fp)+               configure (Just fp)+               go++-- | Get the configuration path.+getConfigPath :: Maybe FilePath -> IO FilePath+getConfigPath mfp = do+  defp <- fmap (</> ".fpco-api.conf") getHomeDirectory+  return (fromMaybe defp mfp)++-- | Read the configuration file.+readConfig :: Monad m => Maybe Text -> String -> m Config+readConfig agent contents = do+  case config of+    Left cperr   -> error $ show cperr+    Right config' -> return config'++  where config = do+          c <- readstring emptyCP contents+          Config <$> get c "API" "token"+                 <*> get c "API" "url"+                 <*> get c "SERVER" "port"+                 <*> pure (fromMaybe (configAgent def) agent)+                 <*> pure (configDebug def)+                 <*> pure (configStartServer def)
+ src/library/FFI.hs view
@@ -0,0 +1,5 @@+-- | A helper module which can be compiled for Haskell and ignored for Fay.++module FFI (module Fay.FFI) where++import Fay.FFI
+ src/library/FP/API.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE NoMonomorphismRestriction #-}+{-# LANGUAGE TemplateHaskell #-}+{-# OPTIONS -fno-warn-missing-signatures #-}++-- | Commands for the IDE API.++module FP.API where++import FP.API.Types+import FP.API.TH++-- General project operations++cmd 'GetInitialProjectInfo+cmd 'GetProjectId++-- Polling++cmd 'GetProjectMessages++-- Module manipulation++cmd 'GetFile+cmd 'GetFileToken+cmd 'SaveFile+cmd 'AddFile+cmd 'DeleteFile++-- Query features++cmd 'GetTypeInfo+cmd 'GetDefinitionSource+cmd 'GetAutocompletions+cmd 'IdeHoogleSearch
+ src/library/FP/API/Convert.hs view
@@ -0,0 +1,234 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE OverloadedStrings  #-}+{-# LANGUAGE PatternGuards      #-}+{-# LANGUAGE TupleSections      #-}+{-# OPTIONS -fno-warn-type-defaults #-}++-- | Convert a Haskell value to a (JSON representation of a) Fay value.++module FP.API.Convert+  (showToFay'+  ,readFromFay')+  where++import           Control.Applicative+import           Control.Monad+import           Control.Monad.State+import           Data.Aeson+import           Data.Attoparsec.Number+import           Data.Char+import           Data.Data+import           Data.Function+import           Data.Generics.Aliases+import           Data.HashMap.Strict    (HashMap)+import qualified Data.HashMap.Strict    as Map+import           Data.Maybe+import           Data.Text              (Text)+import qualified Data.Text              as Text+import           Data.Vector            (Vector)+import qualified Data.Vector            as Vector+import           Numeric+import           Safe+import qualified Text.Show.Pretty       as Show++--------------------------------------------------------------------------------+-- The conversion functions.++-- | Convert a Haskell value to a value representing a Fay value.+showToFay' :: Show a => a -> Maybe Value+showToFay' = Show.reify >=> convert where+  convert value = case value of+    -- Special cases+    Show.Con "True" _    -> return (Bool True)+    Show.Con "False" _   -> return (Bool False)++    -- Objects/records+    Show.Con name values -> fmap (Object . Map.fromList . (("instance",string name) :))+                                 (slots values)+    Show.Rec name fields -> fmap (Object . Map.fromList . (("instance",string name) :))+                                 (mapM (uncurry keyval) fields)+    Show.InfixCons _ _ -> Nothing -- TODO https://github.com/faylang/fay/issues/316++    -- ()+    Show.Tuple [] -> return Null++    -- List types+    Show.Tuple values -> fmap (Array . Vector.fromList) (mapM convert values)+    Show.List values  -> fmap (Array . Vector.fromList) (mapM convert values)++    -- Text types+    Show.String chars -> fmap string (readMay chars)+    Show.Char char    -> fmap (string.return) (readMay char)++    -- Numeric types (everything treated as a double)+    Show.Neg{}     -> double <|> int+    Show.Integer{} -> int+    Show.Float{}   -> double+    Show.Ratio{}   -> double+    where double = convertDouble value+          int = convertInt value++  -- Number converters+  convertDouble = fmap (Number . D) . pDouble+  convertInt = fmap (Number . I) . pInt++  -- Number parsers+  pDouble :: Show.Value -> Maybe Double+  pDouble value = case value of+    Show.Float str   -> getDouble str+    Show.Ratio x y   -> liftM2 (on (/) fromIntegral) (pInt x) (pInt y)+    Show.Neg str     -> fmap (* (-1)) (pDouble str)+    _ -> Nothing+  pInt value = case value of+    Show.Integer str -> getInt str+    Show.Neg str     -> fmap (* (-1)) (pInt str)+    _ -> Nothing++  -- Number readers+  getDouble :: String -> Maybe Double+  getDouble = fmap fst . listToMaybe . readFloat+  getInt :: String -> Maybe Integer+  getInt = fmap fst . listToMaybe . readInt 10 isDigit charToInt+    where charToInt c = fromEnum c - fromEnum '0'++  -- Utilities+  string = String . Text.pack+  slots = zipWithM keyval (map (("slot"++).show) [1::Int ..])+  keyval key val = fmap (Text.pack key,) (convert val)++-- | Convert a value representing a Fay value to a Haskell value.+readFromFay' :: Data a => Value -> Maybe a+readFromFay' value =+  parseDataOrTuple value+  `ext1R` parseArray value+  `extR` parseDouble value+  `extR` parseInt value+  `extR` parseInteger value+  `extR` parseBool value+  `extR` parseString value+  `extR` parseChar value+  `extR` parseText value+  `extR` parseUnit value++-- | Parse a data type or record or tuple.+parseDataOrTuple :: Data a => Value -> Maybe a+parseDataOrTuple value = result where+  result = getAndParse value+  typ = dataTypeOf (fromJust result)+  getAndParse x =+    case x of+      Object obj -> parseObject typ obj+      Array tuple -> parseTuple typ tuple+      _ -> mzero++-- | Parse a tuple.+parseTuple :: Data a => DataType -> Vector Value -> Maybe a+parseTuple typ arr =+  case dataTypeConstrs typ of+    [cons] -> evalStateT (fromConstrM (do i:next <- get+                                          put next+                                          value <- lift (Vector.indexM arr i)+                                          lift (readFromFay' value))+                                      cons)+                         [0..]+    _ -> Nothing++-- | Parse a data constructor from an object.+parseObject :: Data a => DataType -> HashMap Text Value -> Maybe a+parseObject typ obj = listToMaybe (catMaybes choices) where+  choices = map makeConstructor constructors+  constructors = dataTypeConstrs typ+  makeConstructor cons = do+    name <- Map.lookup (Text.pack "instance") obj >>= parseString+    guard (showConstr cons == name)+    if null fields+      then makeSimple obj cons+      else makeRecord obj cons fields++      where fields = constrFields cons++-- | Make a simple ADT constructor from an object: { "slot1": 1, "slot2": 2} -> Foo 1 2+makeSimple :: Data a => HashMap Text Value -> Constr -> Maybe a+makeSimple obj cons =+  evalStateT (fromConstrM (do i:next <- get+                              put next+                              value <- lift (Map.lookup (Text.pack ("slot" ++ show i)) obj)+                              lift (readFromFay' value))+                          cons)+             [1..]++-- | Make a record from a key-value: { "x": 1 } -> Foo { x = 1 }+makeRecord :: Data a => HashMap Text Value -> Constr -> [String] -> Maybe a+makeRecord obj cons fields =+  evalStateT (fromConstrM (do key:next <- get+                              put next+                              value <- lift (Map.lookup (Text.pack key) obj)+                              lift (readFromFay' value))+                          cons)+             fields++-- | Parse a double.+parseDouble :: Value -> Maybe Double+parseDouble value = do+  number <- parseNumber value+  case number of+    I n -> return (fromIntegral n)+    D n -> return n++-- | Parse an int.+parseInt :: Value -> Maybe Int+parseInt value = do+  number <- parseNumber value+  case number of+    I n -> return (fromIntegral n)+    _ -> mzero++-- | Parse an integer.+parseInteger :: Value -> Maybe Integer+parseInteger value = do+  number <- parseNumber value+  case number of+    I n -> return n+    _ -> mzero++-- | Parse a number.+parseNumber :: Value -> Maybe Number+parseNumber value = case value of+  Number n -> return n+  _ -> mzero++-- | Parse a bool.+parseBool :: Value -> Maybe Bool+parseBool value = case value of+  Bool n -> return n+  _ -> mzero++-- | Parse a string.+parseString :: Value -> Maybe String+parseString value = case value of+  String s -> return (Text.unpack s)+  _ -> mzero++-- | Parse a char.+parseChar :: Value -> Maybe Char+parseChar value = case value of+  String s | Just (c,_) <- Text.uncons s -> return c+  _ -> mzero++-- | Parse a Text.+parseText :: Value -> Maybe Text+parseText value = case value of+  String s -> return s+  _ -> mzero++-- | Parse an array.+parseArray :: Data a => Value -> Maybe [a]+parseArray value = case value of+  Array xs -> mapM readFromFay' (Vector.toList xs)+  _ -> mzero++-- | Parse unit.+parseUnit :: Value -> Maybe ()+parseUnit value = case value of+  Null -> return ()+  _ -> mzero
+ src/library/FP/API/Run.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE DeriveDataTypeable    #-}+{-# LANGUAGE FlexibleContexts      #-}+{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings     #-}+{-# LANGUAGE QuasiQuotes           #-}+{-# LANGUAGE RecordWildCards       #-}+{-# LANGUAGE TemplateHaskell       #-}++-- | Function for running arbitrary IDE API commands.++module FP.API.Run+  where++import           FP.API.Convert+import           FP.API.Types++import           Control.Exception+import           Control.Failure+import           Control.Monad.Extra+import           Control.Monad.Logger+import           Control.Monad.Reader+import           Control.Monad.Trans.Resource+import           Data.Aeson                   (decode, encode)+import           Data.ByteString.Lazy         (toChunks)+import           Data.Data+import           Data.IORef+import           Data.Monoid+import           Data.String+import           Data.Text                    (Text)+import qualified Data.Text                    as T+import           Data.Text.Encoding           (encodeUtf8)+import           Language.Fay.Yesod           (Returns (..))+import           Network.HTTP.Conduit+import           Network.HTTP.Types.Status+import           Prelude hiding (catch)+import           Texts.English++-- | Only used internally for encoding.+data Command = IdeCommand IdeCommand+  deriving Show++-- | Monad that can get info for sending commands.+class (Failure HttpException m,MonadIO m,Functor m) => MonadClient m where+  getClientConfig :: m ClientConfig++-- | Simple command configuration.+data ClientConfig = CC+  { ccUrl       :: String+  , ccToken     :: String+  , ccManager   :: Manager+  , ccCookie    :: IORef CookieJar+  , ccUserAgent :: Text+  }++data CommandException+  = CommandException Text+   deriving (Show,Typeable)+instance Exception CommandException++-- | Helpful simple client instance.+instance (Failure HttpException m,MonadIO m,Functor m) => MonadClient (ReaderT ClientConfig m) where+  getClientConfig = ask++-- | Run the given command.+runCommand :: (MonadLogger m,MonadClient m,Data a,Show a) => (Returns' a -> IdeCommand) -> m a+runCommand cmd = do+  CC{..} <- getClientConfig+  request <- parseUrl (ccUrl <> "/fay-command")+  jar <- io $ readIORef ccCookie+  let req = urlEncodedBody params $ setup ccToken request (Just jar) ccUserAgent+  $(logDebug) ("=> " <> trunc (T.pack (show (IdeCommand (cmd Returns)))))+  resp <- io (catch (runResourceT (httpLbs req ccManager))+                    handle301)+  io $ writeIORef ccCookie (responseCookieJar resp)+  case statusCode (responseStatus resp) of+    200 -> do+      case decode (responseBody resp) >>= readFromFay' of+        Nothing -> do $(logDebug) (errprefix <> "<= [unable to decode] " <> T.pack (show (responseBody resp)))+                      error $ "Unable to decode response: " ++ show (responseBody resp)+        Just result ->+          case result of+            Failure e -> do $(logDebug) (errprefix <> "<= " <> trunc e)+                            throw (CommandException e)+            Success a -> do $(logDebug) ("<= " <> trunc (T.pack (show a)))+                            return a+    code -> throw (CommandException ("Bad status code returned from client command: " <> T.pack (show code)))++  where params =+          [("json",mconcat (toChunks (encode (showToFay' (IdeCommand (cmd Returns))))))]+        setup token req jar agent =+          req { method = "POST"+              , redirectCount = 0+              , requestHeaders = requestHeaders req +++                                 [("Accept","application/json")+                                 ,("User-Agent",encodeUtf8 ("fpco-api:" <> agent))+                                 ,("authorization","token " <> fromString token)]+              , responseTimeout = Nothing+              , cookieJar = jar+              }+        trunc :: Text -> Text+        trunc = ellipsize 140+        errprefix = "Error from request: " <> T.pack (show (cmd Returns)) <> "\n"+        handle301 e =+          case e of+            StatusCodeException status _ _ | statusCode status == 301 ->+              throw (CommandException "Got a redirect. Probably an invalid authorization token.")+            _ -> throw e
+ src/library/FP/API/TH.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TemplateHaskell #-}+{-# OPTIONS -fno-warn-orphans #-}++-- | Macro for generating a function out of a command+-- constructor. Only a subset of the IDE commands are of interest for+-- editors and general public use, so we use this command, and we can+-- also write haddock documentation.++module FP.API.TH+  (cmd+  ) where++import FP.API.Run+import FP.API.Types++import Data.Char+import Language.Haskell.TH++cmd :: Name -> Q [Dec]+cmd name = do+  DataConI _name typ _ptyp _fixity <- reify name+  return [FunD funName+               [Clause [] (NormalB (stripIdeCommand typ)) []]]++  where funName = mkName (decapitalize (nameBase name))+        stripIdeCommand = go (1 :: Int) where+          go i (AppT (AppT ArrowT _x) (ConT t))+            | t == ''IdeCommand =+              (AppE (VarE 'runCommand)+                    (foldl (\inner i' -> AppE inner (VarE (gensym i')))+                           (ConE name)+                           [1..i-1]))+          go i (AppT _x y) = LamE [VarP (gensym i)] (go (i+1) y)+          go _ _          = error "invalid IdeCommand constructor type"+        gensym i = mkName ("x" ++ show i)++decapitalize :: String -> String+decapitalize [] = []+decapitalize (x:xs) = toLower x : xs
+ src/library/FP/API/Types.hs view
@@ -0,0 +1,779 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE CPP #-}++module FP.API.Types where++import Data.Data+import Language.Fay.Yesod+import Prelude+import FFI++#ifndef FAY+import Data.Default (Default(..))+import Database.Persist.Sql (PersistField, PersistFieldSql)+import Database.Persist.TH (derivePersistField)+import System.Random (Random)+#endif++data FayProjectId = FayProjectId { unFayProjectId :: Text }+    deriving (Read, Typeable, Data, Show, Eq)++data ProjectState = Workspaces | UserState+#ifdef FAY+    deriving (Read, Typeable, Data, Show, Eq)+#else+    deriving (Show, Eq, Read, Data, Typeable, Enum, Bounded, Ord)+derivePersistField "ProjectState"+#endif++data MergeModifyKind = Modified | Added | Deleted | TypeChanged+#ifdef FAY+    deriving (Read, Typeable, Data, Show, Eq)+#else+    deriving (Show, Eq, Read, Data, Typeable, Enum, Bounded, Ord)+derivePersistField "MergeModifyKind"+#endif++data MergeModifyPair = MergeModifyPair MergeModifyKind MergeModifyKind+#ifdef FAY+    deriving (Read, Typeable, Data, Show, Eq)+#else+    deriving (Show, Eq, Read, Data, Typeable, Bounded)+derivePersistField "MergeModifyPair"+#endif++data FayModuleId = FayModuleId (Maybe FayFileName)+                               FayModuleName+                               PackageId+    deriving (Read, Typeable, Data, Show, Eq)++data FayRunConfigId = FayRunConfigId { unFayRunConfigId :: Text }+    deriving (Read, Typeable, Data, Show, Eq)++data CanFail a = Success (Automatic a) | Failure Text+    deriving (Read, Typeable, Data, Show, Eq)+#ifndef FAY+deriving instance Functor CanFail+#endif++type Returns' a = Returns (CanFail a)++data IdeCommand+    -- User profile+    = SaveProfile            Theme Int Bool                                    (Returns' ())++    -- Project global+    | GetInitialProjectInfo                                                    FayProjectId (Returns' InitialProjectInfo)+    | ReparseProjectSettings                                                   FayProjectId (Returns' ReparseSettingsOutput)+    | GetSettings                                                              FayProjectId (Returns' GetSettingsOutput)+    | SetSettings            SetSettingsInput                                  FayProjectId (Returns' (Maybe CompileId))++    | GetKeterYaml           FayDeploymentId FayModuleName                     FayProjectId (Returns' KeterYaml)+    | SetRunConfigs          [(FayRunConfigId,RunConfig)]                      FayProjectId (Returns' ())+    | SetDeployments         [(FayDeploymentId,Deployment)]                    FayProjectId (Returns' ())+    | GetNewRunConfig                                                          FayProjectId (Returns' NewRunConfig)+    | GetNewDeployment                                                         FayProjectId (Returns' NewDeployment)++    | GetNewWebApp           FayDeploymentId                                   FayProjectId (Returns' NewWebApp)+    | GetNewBgJob            FayDeploymentId                                   FayProjectId (Returns' NewBgJob)+    | SetPublic              Publicize                                         FayProjectId (Returns' (Maybe GitHistoryItem))+    | SetProjectMetadata     Text Text                                         FayProjectId (Returns' ())+    | DeleteProject          Text                                              FayProjectId (Returns' ())++    -- Host names+    | CheckHostName          Text                                              FayProjectId (Returns' UseHostName)+    | GetRandomHostName                                                        FayProjectId (Returns' RandomHostName)+    | GetDeploymentManagerInfo                                                 FayProjectId (Returns' DeploymentManagerInfo)++    -- User State+    | SaveProjectState       ProjectState Text                                 FayProjectId (Returns' ())+    | LoadProjectState       ProjectState                                      FayProjectId (Returns' MaybeText)++    -- Files+    | AddFile                FileInfo                                          FayProjectId (Returns' SaveFileOutput)+    | SaveFile               FayFileName Text FayTutorialToken                 FayProjectId (Returns' SaveFileOutput)+    | DeleteFile             FayFileName                                       FayProjectId (Returns' CompileChanged)+    | RenameFile             FayFileName FileInfo RenameType                   FayProjectId (Returns' RenameFileOutput)+    | GetFile                FayFileName                                       FayProjectId (Returns' FileContent)+    | GetFileToken           FayFileName                                       FayProjectId (Returns' FayTutorialToken)+    | GetAllFiles                                                              FayProjectId (Returns' [(FayFileName, ModuleIncluded)])+    | SetModuleExclusion     FayFileName Bool                                  FayProjectId (Returns' CompileChanged)++    -- Info+    | GetTypeInfo            SourceSpan Int                                    FayProjectId (Returns' ())+    | GetDefinitionSource    SourceSpan                                        FayProjectId (Returns' ())+    | GetAutocompletions     AutoCompleteInput                                 FayProjectId (Returns' ())+--    | GetTopLevelIdentifiers FayFileName                                     FayProjectId (Returns' TopLevelIdentifiers)+    | IdeHoogleSearch        (Maybe FayFileName) Bool Int Int Int Text         FayProjectId (Returns' HoogleId)+        -- ^ module context, is it exact?, number to query, offset in result, number to yield, query contents+    | GetProjectMessages     ProjectMessagesRequest                            FayProjectId (Returns' ProjectMessagesOutput)++    -- Compilation+    | SetTarget              (Maybe (Either FayFileName FayRunConfigId))       FayProjectId (Returns' CompileChanged)+    | GetTarget                                                                FayProjectId (Returns' (Maybe (Either FayFileName FayRunConfigId)))+    | RunTarget                                                                FayProjectId (Returns' ProcId)+    --TODO: Make these use the current target.+    | CompileBinary          FayFileName [(Text, Text)]                        FayProjectId (Returns' BuildId)+    | UploadBuild            BuildResult                                       FayProjectId (Returns' UploadedBuild)++    -- Git+    | CommitToGit            Text                                              FayProjectId (Returns' ())+    | GetGitHistory          Int Int                                           FayProjectId (Returns' GitHistory)+    | IsProjectDirty                                                           FayProjectId (Returns' Bool)+    | ResetProject                                                             FayProjectId (Returns' InitialProjectInfo)+    | SetRoot                Text Text                                         FayProjectId (Returns' InitialProjectInfo)++    | GitPush                Text                                              FayProjectId (Returns' ()) -- ^ Text == URL+    | GitPull                Text                                              FayProjectId (Returns' GitPullResult) -- ^ Text == URL+    | GitMergeAbort          Text                                              FayProjectId (Returns' InitialProjectInfo) -- ^ Text == URL+    | GitMergeDone           Text MaybeText                                    FayProjectId (Returns' GitResolvedResult) -- ^ Text == URL, Msg+    | GitResolveFile         FayFileName                                       FayProjectId (Returns' ())++    | GitDiff                                                                  FayProjectId (Returns' Text)++    -- Git Remotes+    | SetRemotes             RemotesList                                       FayProjectId (Returns' ())++    -- Git Branches+    | CheckoutBranch         Text                                              FayProjectId (Returns' GitCheckoutResult)+    | CreateBranch           Text Text                                         FayProjectId (Returns' ())+    | DeleteBranch           Text                                              FayProjectId (Returns' ())+    | BranchFromCommit       Text Text                                         FayProjectId (Returns' ())++    -- Github+    | UserAuthedGithub (Returns (CanFail Bool))+    | RevokeGithub (Returns (CanFail ()))+    | GetGithubUrl Text (Returns (CanFail Text))+    | SshPublicKey (Returns (CanFail Text))++    -- Module manipulation+    | ReformatModule         FayFileName                                       FayProjectId (Returns' CompileChanged)++    -- Configuration+    | GetConfigurationProject                                                               (Returns' Text)+    | GenerateConfiguration                                                                 (Returns' ())+    | SetConfigurationJavascript Text                                                       (Returns' ())++    -- Misc+    | RenderFileMarkdown     FayFileName                                       FayProjectId (Returns' HtmlReply)+    | GetTrialExpiry                                                                        (Returns' ExpiryTime)+    | ShowTrialSignup                                                                       (Returns' Bool)+    | RestartBackend                                                           FayProjectId (Returns' ())+--    | RunGhci Bool                                                             FayProjectId (Returns' RunGhciOutput)+    | SearchProject          SearchQuery Int Int Bool                          FayProjectId (Returns' ())+    | CloseAllProjects                                                                      (Returns' ())+    | SdistTarball                                                             FayProjectId (Returns' ())+    | GetProjectId           Text                                                           (Returns' FayProjectId)+    deriving (Read, Typeable, Data, Show, Eq)+++-- | Values passed to the client when initially loading the IDE.+data InitialProjectInfo = InitialProjectInfo+    { ipiTitle :: Text+    , ipiDesc :: Text+    , ipiGitUrl :: Maybe Text -- ^ URL originally cloned from+    , ipiMergeConflicts :: Maybe [MergeConflict]+    , ipiInvalidSettings :: Bool+    , ipiState :: [(ProjectState, Text)]+    , ipiFiles :: [(FayFileName, ModuleIncluded)]+    , ipiTarget :: Maybe (Either FayFileName FayRunConfigId)+    , ipiPublished :: Maybe GitHistoryItem+    , ipiBranches :: BranchesList+    , ipiRemotes :: RemotesList+    , ipiRunConfigs :: [(FayRunConfigId,RunConfig)]+    , ipiDeployments :: [(FayDeploymentId,Deployment)]+    , ipiTheme :: Theme+    , ipiFontSize :: Int+    , ipiSearchWithRegex :: Bool+    , ipiLicense :: IdeLicense+    , ipiCanPublish :: Bool+    }+    deriving (Read, Typeable, Data, Show, Eq)++data IdeLicense = ILCommunity | ILPersonal | ILProfessional+    deriving (Read, Typeable, Data, Show, Eq)++-- TODO: middle text should be the code that the type comes from.+data TypeInfo = TypeInfo SourceSpan Text Text+    deriving (Read, Typeable, Data, Show, Eq)++data MaybeText = NoText | JustText Text -- FAY BUG+    deriving (Read, Typeable, Data, Show, Eq)++-- | Result of checking out a branch (or ref, in future).+data GitCheckoutResult+  = GCRDirty+  | GCROk (Maybe InitialProjectInfo)+    deriving (Read, Typeable, Data, Show, Eq)++data GitPullResult = GPRSuccess InitialProjectInfo+                   | GPRDirtyTree+                   | GPRManualMerge Text InitialProjectInfo+    deriving (Read, Typeable, Data, Show, Eq)++data GitResolvedResult = GRRSuccess+                       | GRRStillUnresolved [MergeConflict]+    deriving (Read, Typeable, Data, Show, Eq)++data GitMergeConflictsResult = GitMergeConflictsResult [MergeConflict]+    deriving (Read, Typeable, Data, Show, Eq)++data MergeConflict = MergeConflict+    { mergeFile :: FayFileName+    , mergeState :: MergeModifyPair+    }+    deriving (Read, Typeable, Data, Show, Eq)++data GitHistory = GitHistory [GitHistoryItem] -- FAY BUG+    deriving (Read, Typeable, Data, Show, Eq)++data GitHistoryItem = GitHistoryItem+    { ghiDate :: Text+    , ghiAuthor :: Text+    , ghiLog :: Text+    , ghiHash :: Text+    }+    deriving (Read, Typeable, Data, Show, Eq)++data RemotesList = RemotesList [(Text,Text)]+  deriving (Read, Typeable, Data, Show, Eq)++data BranchesList = BranchesList Text [Text]+  deriving (Read, Typeable, Data, Show, Eq)++data ReparseSettingsOutput+    = SettingsAlreadyValid+    | ReparseSuccessful InitialProjectInfo+    deriving (Read, Typeable, Data, Show, Eq)++data GetSettingsOutput = GetSettingsOutput+    { gsoModuleTemplate :: Text+    , gsoExtensions     :: [(Text, Maybe Bool)] -- ^ three states: on, off, or default (== Nothing)+    , gsoEnvironment    :: Environment+    , gsoEnvironments   :: [Environment]+    , gsoGhcArgs        :: [Text]+    , gsoExtraPackages  :: Text+    , gsoHiddenPackages :: Text+    , gsoCabalName      :: Text+    , gsoCabalVersion   :: Text+    , gsoRoot           :: Text+    , gsoFilters        :: Text+    }+    deriving (Read, Typeable, Data, Show, Eq)++data SetExtension = SetExtension Text Bool+    deriving (Read, Typeable, Data, Show, Eq)++data SetSettingsInput = SetSettingsInput+    { ssiModuleTemplate :: Text+    , ssiExtensions     :: [SetExtension]+    , ssiEnvironment    :: Text+    , ssiGhcArgs        :: [Text]+    , ssiExtraPackages  :: Text+    , ssiHiddenPackages :: Text+    , ssiCabalName      :: Text+    , ssiCabalVersion   :: Text+    }+    deriving (Read, Typeable, Data, Show, Eq)++-- | A GHC environment.+data Environment = Environment+  { envName :: Text+  , envTitle :: Text+  , envURL :: Text+  }+  deriving (Read, Typeable, Data, Show, Eq)++data RunGhciOutput = RunGhciOutput ProcId FayProjectId+    deriving (Read, Typeable, Data, Show, Eq)++data TopLevelIdentifiers = TopLevelIdentifiers [TopLevelIdentifier]+    deriving (Read, Typeable, Data, Show, Eq)++data TopLevelIdentifier = TopLevelIdentifier+    { tliLine :: Int+    , tliColumn :: Int+    , tliName :: Text+    }+    deriving (Read, Typeable, Data, Show, Eq)++-- Copied from ide-backend/Common.hs+--  + Different deriving list+--  + 'Show' instances made into functions++data SourceSpan = SourceSpan+  { spanFilePath   :: FayFileName+  , spanFromLine   :: Int+  , spanFromColumn :: Int+  , spanToLine     :: Int+  , spanToColumn   :: Int }+  deriving (Read, Typeable, Data, Show, Eq)++data EitherSpan =+    ProperSpan SourceSpan+  | TextSpan String+  deriving (Read, Typeable, Data, Show, Eq)++-- | An error or warning in a source module.+--+-- Most errors are associated with a span of text, but some have only a+-- location point.+--+data SourceInfo = SourceInfo+  { infoKind :: SourceInfoKind+  , infoSpan :: EitherSpan+  , infoMsg  :: Text+  }+  deriving (Read, Typeable, Data, Show, Eq)++-- | Severity of a piece of info.+data SourceInfoKind = KindError | KindWarning | KindHint+  deriving (Read, Typeable, Data, Show, Eq)++-- | Is a target that we're running a web service? We're not sure that+-- it's not, but if the port is open, we're confident that it is.+data IsWebResult+  = IsWeb+  | NotSureIfWeb+  deriving (Read, Typeable, Data, Show, Eq)++-- | A simple text reply.+data TextReply+  = TextReply { unTextReply :: Text }+  deriving (Read, Typeable, Data, Show, Eq)++-- | An html reply.+data HtmlReply+  = HtmlReply { unHtmlReply :: Text }+  deriving (Read, Typeable, Data, Show, Eq)++-- | Indicates the state of a starting project. Each request can either+-- indicate that there is more data coming, or that this is the final status.+data ProjectStartStatus = PSSUpdate Text Int | PSSFinal Text+  deriving (Read, Typeable, Data, Show, Eq)++data MaybeStartToken = NoStartToken | StartToken Int+  deriving (Read, Typeable, Data, Show, Eq)++-- | A run configuration for a project.+data RunConfig = RunConfig+  { rcTitle :: Text+  , rcMainFile :: Maybe FayFileName+  , rcArgs :: [Text]+  , rcEnv :: [(Text,Text)]+  } deriving (Read, Typeable, Data, Show, Eq)++-- | Make a new run configuration.+data NewRunConfig = NewRunConfig (FayRunConfigId,RunConfig)+  deriving (Read, Typeable, Data, Show, Eq)++-- | A deployment configuration.+data Deployment = Deployment+  { depTitle :: Text+  , depStanzas :: [Stanza]+  } deriving (Read, Typeable, Data, Show, Eq)++-- | Possible stanza types.+data Stanza = WebAppStanza FayWebAppId WebApp+            | BgJobStanza FayBgJobId BgJob+  deriving (Read, Typeable, Data, Show, Eq)++data NewDeployment = NewDeployment (FayDeploymentId,Deployment)+  deriving (Read, Typeable, Data, Show, Eq)++-- | A web app stanza.+data WebApp = WebApp+  { wapTitle    :: Text+  , wapHostname :: Maybe Text+  , wapFileName :: Maybe FayFileName+  , wapArgs     :: [Text]+  , wapEnv      :: [(Text,Text)]+  , wapSsl      :: Bool+  } deriving (Read, Typeable, Data, Show, Eq)++data NewWebApp = NewWebApp (FayWebAppId,WebApp)+  deriving (Read, Typeable, Data, Show, Eq)++-- | A background job stanza.+data BgJob = BgJob+  { bgTitle        :: Text+  , bgFileName     :: Maybe FayFileName+  , bgArgs         :: [Text]+  , bgEnv          :: [(Text,Text)]+  , bgRestartLimit :: Maybe Int+  , bgRestartDelay :: Int+  } deriving (Read, Typeable, Data, Show, Eq)++-- | Make a new background job.+data NewBgJob = NewBgJob (FayBgJobId,BgJob)+  deriving (Read, Typeable, Data, Show, Eq)++-- | Result of trying to use a hostname.+data UseHostName = HostnameInUse         -- ^ Host name is in use by someone else, can't be used.+                 | HostnameOK            -- ^ Host name was already or has now been registered and is now in use.+                 | HostnameQuotaExcess   -- ^ Couldn't register the hostname due to quota.+                 | HostnameInvalid       -- ^ Invalid hostname.+  deriving (Read, Typeable, Data, Show, Eq)++-- | Yaml text for a Keter config.+data KeterYaml = KeterYaml+    { keterYaml :: Text+    , deployYaml :: Text+    }+  deriving (Read,Typeable,Data,Show,Eq)++-- | A randomly generated host name.+data RandomHostName = RandomHostName { unRandomHostname :: Text }+  deriving (Read,Typeable,Data,Show,Eq)++-- | A date of expiration, if any.+data ExpiryTime = ExpiryTime (Maybe Integer)+  deriving (Read,Typeable,Data,Show,Eq)++data DeploymentManagerInfo = DeploymentManagerInfo+  { dmiHostname :: Text }+  deriving (Read,Typeable,Data,Show,Eq)++data FayManualMergeId = FayManualMergeId { unFayManualMergeId :: Text }+    deriving (Read, Typeable, Data, Show, Eq)++data FayDeploymentId = FayDeploymentId { unFayDeploymentId :: Text }+    deriving (Read, Typeable, Data, Show, Eq)++data FayBgJobId = FayBgJobId { unFayBgJobId :: Text }+    deriving (Read, Typeable, Data, Show, Eq)++data FayWebAppId = FayWebAppId { unFayWebAppId :: Text }+    deriving (Read, Typeable, Data, Show, Eq)++--------------------------------------------------------------------------------+-- Themes++-- | Themes supported by the IDE.+data Theme = Panda | Zenburn | Monokai+#ifdef FAY+  deriving (Read,Typeable,Data,Show,Eq)+#else+  deriving (Show, Eq, Read, Data, Typeable, Bounded, Enum)+#endif++--------------------------------------------------------------------------------+-- Search++data SearchQuery+  = SearchQueryRegex Text+  | SearchQueryPlain Text+  deriving (Read,Typeable,Data,Show,Eq)++--------------------------------------------------------------------------------+-- Publication++data Publicize = NotPublic+               | Publicize Text+  deriving (Read,Typeable,Data,Show,Eq)++--------------------------------------------------------------------------------+-- Files / Modules++data FayFileName = FayFileName { unFayFileName :: Text }+    deriving (Read, Typeable, Data, Show, Eq)++data FayModuleName = FayModuleName { unFayModuleName :: Text }+    deriving (Read, Typeable, Data, Show, Eq)++data FileInfo = FileInfo+    { fiPath :: FayFileName+    , fiModule :: Maybe FayModuleName+    }+    deriving (Read, Typeable, Data, Show, Eq)++data FileChanged = FileChanged+    { fcPath :: FayFileName+    , fcModule :: Maybe ModuleIncluded+    }+    deriving (Read, Typeable, Data, Show, Eq)++data ModuleIncluded+    = ModuleExcluded+    | ModuleWrongExtension+    | ModuleNotTextual+    | ModuleHeaderFilenameMismatch FayModuleName+    | ModuleNameAmbiguous FayModuleName+    | ModuleIncluded FayModuleName+    deriving (Read, Typeable, Data, Show, Eq)++data RenameType = RenamePlain | RenameHeader | RenameHeaderAndImports | RenameHeaderAndImportsForce+    deriving (Read, Typeable, Data, Show, Eq)++data CompileChanged = CompileChanged+    { ccCompileId :: Maybe CompileId+    , ccFiles :: [FileChanged] -- ^ All of the 'FileChanged's that have a+                                 -- 'fiModule' that's changed.+    }+    deriving (Read, Typeable, Data, Show, Eq)++data RenameFileOutput+    = RenameFileOutput (Maybe Text) CompileChanged+    | WarnImportRenaming [FayFileName]+    deriving (Read, Typeable, Data, Show, Eq)++data SaveFileOutput = SaveFileOutput FayTutorialToken CompileChanged+    deriving (Read, Typeable, Data, Show, Eq)++data FileContent = FileContent+    { dfcContent :: Maybe Text+    , dfcToken :: FayTutorialToken+    }+    deriving (Read, Typeable, Data, Show, Eq)++--------------------------------------------------------------------------------+-- Version token++-- | A token for the tutorial.+type FayTutorialToken = TutorialConcurrentToken++#ifdef FAY+data TutorialConcurrentToken = TutorialConcurrentToken'+    { unTutorialConcurrentToken :: Int }+    deriving (Eq, Show, Data, Read, Typeable)+#else+-- | Token for a tutorial.+newtype TutorialConcurrentToken = TutorialConcurrentToken'+    { unTutorialConcurrentToken :: Int }+    deriving (Eq, Show, Data, Read, Typeable, PersistField, PersistFieldSql, Random, Num)++instance Default TutorialConcurrentToken where+    def = TutorialConcurrentToken' 1+#endif++--------------------------------------------------------------------------------+-- Isolation-runner ids++data CompileId = CompileId { unCompileId :: Int }+    deriving (Read, Typeable, Data, Show, Eq)++data ProcId = ProcId { unProcId :: Int }+    deriving (Read, Typeable, Data, Show, Eq)++data BuildId = BuildId { unBuildId :: Int }+    deriving (Read, Typeable, Data, Show, Eq)++data HoogleId = HoogleId { unHoogleId :: Int }+    deriving (Read, Typeable, Data, Show, Eq)++data SearchId = SearchId { unSearchId :: Int }+    deriving (Read, Typeable, Data, Show, Eq)++--------------------------------------------------------------------------------+-- Isolation-runner messages++data ProjectMessagesRequest+    = PMRImmediateStatusNoMessages+    | PMRImmediateStatusWithMessages ProjectMessagesFilter+    | PMRNextStatusWithMessages ProjectMessagesFilter StatusHash+    deriving (Read, Typeable, Data, Show, Eq)++data ProjectMessagesOutput+    = ProjectMessagesOutput ProjectMessagesFilter [(Maybe Int, RunnerMessage)]+    deriving (Read, Typeable, Data, Show, Eq)++data StatusHash = StatusHash Text+    deriving (Read, Typeable, Data, Show, Eq)++data ProjectMessagesFilter+    = PMFilterNone+    | PMFilterAll+    | PMFilterOnOrBefore Integer+    deriving (Read, Typeable, Data, Show, Eq)++data MessageTag = MessageTag+    { mtProjectId :: Maybe FayProjectId+    --TODO: use a better type than Int.+    , mtJobId     :: Maybe Int+    }+    deriving (Read, Typeable, Data, Show, Eq)++data RunnerMessageEnvelope = RunnerMessageEnvelope+    { rmeSeqNumber  :: Integer+    , rmeMessageTag :: MessageTag+    , rmeMessage    :: RunnerMessage+    }+    deriving (Read, Typeable, Data, Show, Eq)++data RunnerMessage+    = ProjectMessage+        { rmPsLevel   :: LogLevel+        , rmPsMessage :: Text+        }+    | ProcessOutput Text+    | ProcessOutputError Text+    | StatusSnapshot ProjectStatusSnapshot StatusHash+    | IdInfoResults IdInfo+    | SubExprsResults SourceSpan [[TypeInfo]]+    | AutoCompleteResults (Maybe AutoCompleteInput) [Text]+    | ImportedPackagesResults [PackageId]+    | SearchResults [SearchResult]+    | HoogleResults Text [HoogleResult] (Maybe Int)+    | ProjectHasClosed+    | FayCompileResults Text+    deriving (Read, Typeable, Data, Show, Eq)++data LogLevel+    = LevelDebug+    | LevelInfo+    | LevelWarn+    | LevelError+    | LevelOther Text+    deriving (Read, Typeable, Data, Show, Eq)++data ProjectStatusSnapshot = ProjectStatusSnapshot+    { snapOpeningStatus :: RunnerOpeningStatus+    , snapCompileStatus :: RunnerCompileStatus+    , snapProcessStatus :: ProcessStatusSnapshot+    , snapBuildStatus   :: RunnerBuildStatus+    }+    deriving (Read, Typeable, Data, Show, Eq)++data IdInfo+    = NoIdInfo SourceSpan -- ^ query span+    | IdInfo SourceSpan SourceSpan DefinitionSource -- ^ query span, result span, source info+    deriving (Read, Typeable, Data, Show, Eq)++data DefinitionSource = DefinitionLocal SourceSpan+                      | DefinitionTextSpan Text Text+                      | DefinitionImported Text FayModuleId FayModuleId EitherSpan EitherSpan+                      | DefinitionWiredIn Text+                      | DefinitionBinder Text+    deriving (Read, Typeable, Data, Show, Eq)++data AutoCompleteInput = AutoCompleteInput+    { aciModuleName :: FayFileName+    , aciPrefix     :: Text+    }+    deriving (Read, Typeable, Data, Show, Eq)++data PackageId = PackageId+    { packageName    :: Text+    , packageVersion :: Maybe Text+    }+    deriving (Read, Typeable, Data, Show, Eq)++data SearchResult = SearchResult SourceSpan [Either Text Text]+    deriving (Read, Typeable, Data, Show, Eq)++data HoogleResult = HoogleResult+    { hrURL     :: String+    , hrSources :: [(PackageLink, [ModuleLink])]+    , hrTitle   :: String -- ^ HTML+    , hrBody    :: String -- ^ plain text+    }+    deriving (Read, Typeable, Data, Show, Eq)++data PackageLink = PackageLink+    { plName :: String+    , plURL :: String+    }+    deriving (Read, Typeable, Data, Show, Eq)++data ModuleLink = ModuleLink+    { mlName :: String+    , mlURL :: String+    }+    deriving (Read, Typeable, Data, Show, Eq)++data RunnerOpeningStatus+    = RunnerProjectOpening Text+    | RunnerProjectOpen+    deriving (Read, Typeable, Data, Show, Eq)++data RunnerCompileStatus+    = RunnerNotCompiling+    | RunnerCompiling CompileId Progress+    | RunnerCompileDone CompileId [SourceInfo]+    deriving (Read, Typeable, Data, Show, Eq)++data ProcessStatusSnapshot+    = SnapshotNoProcess+    | SnapshotProcessQueued ProcId+    | SnapshotProcessRunning ProcId LaunchInfoSnapshot PortListeningResult+    | SnapshotProcessExited ProcId LaunchInfoSnapshot+    | SnapshotProcessLoadFailed ProcId Text+    | SnapshotProcessRunFailed ProcId Text+    | SnapshotProcessUserException ProcId Text+    | SnapshotProcessGhcException ProcId Text+    | SnapshotProcessForceCanceled ProcId+    deriving (Read, Typeable, Data, Show, Eq)++data RunnerBuildStatus+    = RunnerNotBuilding+    | RunnerBuildQueued+        { rbsId :: BuildId+        }+    | RunnerBuilding+        { rbsId :: BuildId+        , rbsProgress :: Progress+        }+    | RunnerBuildFailed+        { rbsId :: BuildId+        , rbsMsg :: Text+        }+    | RunnerBuildDone+        { rbsId :: BuildId+        , rbsResult :: BuildResult+        }+    deriving (Read, Typeable, Data, Show, Eq)++data BuildResult = BuildResult+    { brPathName   :: Text+    , brMainModule :: Text+    , brFileSize   :: Int+    }+    deriving (Read, Typeable, Data, Show, Eq)++data UploadedBuild = UploadedBuild+    { ubUrl :: Text+    , ubExe :: Text+    }+    deriving (Read, Typeable, Data, Show, Eq)++-- | This type represents intermediate progress information during compilation.+data Progress = Progress+    {+      -- | The current step number+      --+      -- When these Progress messages are generated from progress updates from+      -- ghc, it is entirely possible that we might get step 4/26, 16/26, 3/26;+      -- the steps may not be continuous, might even be out of order, and may+      -- not finish at X/X.+      progressStep :: Int++      -- | The total number of steps+    , progressNumSteps :: Int++      -- | The parsed message. For instance, in the case of progress messages+      -- during compilation, 'progressOrigMsg' might be+      --+      -- > [1 of 2] Compiling M (some/path/to/file.hs, some/other/path/to/file.o)+      --+      -- while 'progressMsg' will just be 'Compiling M'+    , progressMsg :: Text+    }+    deriving (Read, Typeable, Data, Show, Eq)++data LaunchInfoSnapshot = LaunchInfoSnapshot+    { liSnapApproot  :: Maybe Text+    , liSnapPort     :: Maybe Int+    , liSnapLaunched :: Text -- ^ Timestamp+    }+    deriving (Read, Typeable, Data, Show, Eq)++data PortListeningResult+    = ProcessNotRunning+    | PortNotListening+    | PortIsListening+    | PortNotAllocated+    deriving (Read, Typeable, Data, Show, Eq)
+ src/library/FP/Server.hs view
@@ -0,0 +1,510 @@+{-# LANGUAGE DeriveGeneric       #-}+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE QuasiQuotes         #-}+{-# LANGUAGE RecordWildCards     #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell     #-}++-- | Main entry point to the server.++module FP.Server where++import           FP.API+import           FP.API.Run+import           FP.API.Types+import           FP.Server.Spans+import           FP.Server.Types++import           Control.Applicative+import           Control.Concurrent.Lifted+import           Control.Exception.Lifted   hiding (handle)+import           Control.Monad.Extra+import           Control.Monad.Logger+import           Control.Monad.Reader+import           Data.Aeson+import qualified Data.ByteString.Char8      as S8+import           Data.ByteString.Lazy       (fromChunks)+import qualified Data.ByteString.Lazy.Char8 as L8+import           Data.IORef+import qualified Data.Map                   as M+import           Data.Map (Map)+import           Data.Maybe+import           Data.Monoid+import           Data.Text                  (Text, pack, unpack)+import qualified Data.Text                  as T+import           Data.Text.Encoding         (decodeUtf8)+import qualified Data.Text.IO               as T+import           Data.Typeable+import           Network+import           Network.HTTP.Conduit+import           Prelude                    hiding (span,catch)+import           System.Directory+import           System.FilePath+import           System.IO+import           Texts.English++-- | Run the given Server command with the config. Good for testing in the repl.+runWithConfig :: Server b -> Config -> IO b+runWithConfig m config = do+  manager <- newManager def+  pollers <- newMVar mempty+  jar <- newIORef mempty+  tokensVar <- newMVar mempty+  tokens <- newMVar tokensVar+  runReaderT ((if configDebug config then runStderrLoggingT else runSilentLoggingT) m)+             (ServerReader (CC (stripSlash (configUrl config))+                               (configToken config)+                               manager+                               jar+                               (configAgent config))+                           config+                           pollers+                           tokens)++  where runSilentLoggingT m' =+          runLoggingT m' (\_loc _src _lvl _str -> return ())++-- | Start the server.+startServer :: Bool -> Server ()+startServer forkOnceListening = do+  Config{..} <- asks serverConfig+  io (hSetBuffering stdout NoBuffering)+  sock <- io (listenOn (PortNumber (fromIntegral configPort)))+  $(logInfo) ("Server started on port " <> pack (show configPort) <>+              ", remote URL is: " <> pack configUrl)+  (if forkOnceListening then void . fork else id)+    (forever (acceptConnection sock))++-- | Accept a connection on the given socket.+acceptConnection :: Socket -> Server ()+acceptConnection sock = do+  (handle,remote,port) <- io (accept sock)+  io (hSetBuffering handle NoBuffering)+  $(logInfo) ("Connection accepted from: " <> pack remote <>+              ":" <> pack (show port) <>+              " (" <> pack (show handle) <> ")")+  void (fork (handleLine handle))++-- | Handle a line of input.+handleLine :: Handle -> Server ()+handleLine handle = flip finally (close handle) $ do+  result <- io (try (S8.hGetLine handle))+  case result of+    Left (_ :: IOException) -> do+      $(logError) "Unable to get line from handle."+    Right line -> do+      case decode (fromChunks [line]) of+        Nothing -> do+          $(logError) ("Unable to parse JSON from line: " <> decodeUtf8 line)+        Just msg -> do+          $(logDebug) ("<- " <> pack (show msg))+          handleMessage handle msg++-- | Handle any incoming message from the client.+handleMessage :: Handle -> Msg -> Server ()+handleMessage h msg =+  case msg of+    MsgSaveModule fpid root filename ->+      saveTheModule h fpid root filename+    MsgCheckModule fpid root filename path ->+      checkModule h fpid root filename path+    MsgTypeInfo fpid filename sl sc el ec ->+      typeInfo h fpid filename sl sc el ec+    MsgGetDefinition fpid root filename sl sc el ec ->+      getDefinition h fpid root filename sl sc el ec+    MsgAutoComplete fpid filename prefix ->+      autoComplete h fpid filename prefix+    MsgHoogleIdent fpid filename name ->+      hoogleIdent h fpid filename name+    MsgHoogleDb fpid name ->+      hoogleDb h fpid name+    MsgDownloadFiles fpid fp ->+      downloadFiles h fpid fp+    MsgWriteEmacsConfig fpid fp ->+      writeEmacsConfig h fpid fp++-- Message handlers++-- | Write out the .dir-locals.el file for the project.+writeEmacsConfig :: Handle -> Either Text FayProjectId -> FilePath -> Server ()+writeEmacsConfig h pid root = do+  fpid <- getFayProjectId pid+  io (writeFile (root </> ".dir-locals.el")+                (unlines (src fpid)))+  reply h (ReplyOK ())++  where src fpid =+          ["((nil . ((fpco-pid . " ++ unpack (unFayProjectId fpid) ++ ")"+          ,"         (eval . (set (make-local-variable 'fpco-root)"+          ,"                       (expand-file-name"+          ,"                         (locate-dominating-file buffer-file-name \".dir-locals.el\")))))))))"]++-- | Download all files in the project, overwriting any local copies.+downloadFiles :: Handle -> Either Text FayProjectId -> FilePath -> Server ()+downloadFiles h pid root = do+  fpid <- getFayProjectId pid+  ipi <- getInitialProjectInfo fpid+  forM_ (ipiFiles ipi) $ \(name,_) -> do+    FileContent text _ <- getFile name fpid+    io (writeCreateFile (root </> T.unpack (unFayFileName name))+                        (fromMaybe "" text))+  reply h (ReplyOK ())++  where writeCreateFile fp text = do+          io (createDirectoryIfMissing True (takeDirectory fp))+          T.writeFile (root </> fp) text++-- | Hoogle search whole database.+hoogleDb :: Handle -> FayProjectId -> Text -> Server ()+hoogleDb h fpid q = do+  void (ideHoogleSearch Nothing exact count offset limit q fpid)+  wait fpid+       (Callback+          (\_mid msg ->+             case msg of+               HoogleResults _ results _ ->+                   do reply h (ReplyHoogleResults results)+                      return Done+               _ -> return NotDone))++  where exact = False+        count = 10+        offset = 0+        limit = count++-- | Hoogle search for an identifier in a module.+hoogleIdent :: Handle -> FayProjectId -> String -> Text -> Server ()+hoogleIdent h fpid filename q = do+  hid <- ideHoogleSearch (Just mname) exact count offset limit q fpid+  wait fpid+       (Callback+          (\mid msg ->+             case msg of+               HoogleResults _ results _+                 | toHoogleId mid == Just hid ->+                   do case results of+                        (result:_) -> reply h (ReplyHoogleResult result)+                        _ -> return ()+                      return Done+               _ -> return NotDone))++  where mname = FayFileName (pack filename)+        exact = True+        count = 10+        offset = 0+        limit = count+        toHoogleId = fmap HoogleId++-- | Autocomplete the given prefix replying with a list of+-- completions.+autoComplete :: Handle+             -> FayProjectId+             -> FilePath+             -> Text+             -> Server ()+autoComplete h fpid filename prefix = do+  case T.strip prefix of+    "" -> reply h (ReplyCompletions [])+    _ -> do+      getAutocompletions input fpid+      wait fpid+           (Callback+              (\_ msg ->+                 case msg of+                   AutoCompleteResults input' completions+                     | Just input == input' -> do reply h (ReplyCompletions completions)+                                                  return Done+                   _ -> return NotDone))++  where input = AutoCompleteInput (FayFileName (pack filename))+                                  prefix++-- | Get the definition location of the identifier at the given span.+getDefinition :: Handle+              -> FayProjectId -> FilePath+              -> FilePath+              -> Int -> Int+              -> Int -> Int+              -> Server ()+getDefinition h fpid root filename sl sc el ec = do+  getDefinitionSource defSpan fpid+  wait fpid+       (Callback (\_ msg ->+                    case msg of+                      IdInfoResults i ->+                        case i of+                          NoIdInfo src+                            | src == defSpan -> return Done+                          IdInfo src _ info+                            | src == defSpan -> do reply h (ReplyLocation (makeDef info))+                                                   return Done+                          _ -> return NotDone+                      _ -> return NotDone))++  where defSpan =+           SourceSpan (FayFileName (pack filename))+                      sl sc+                      el ec+        makeDef (DefinitionLocal span) =+          DefinitionLoc (makeLoc root span)+        makeDef (DefinitionTextSpan t1 t2) =+          DefinitionUseless (t1 <> " " <> t2 <> " (nowhere known to go)")+        makeDef (DefinitionImported text m1 m2 es1 es2) =+          DefinitionImport text+                           (makeModuleId m1)+                           (makeModuleId m2)+                           (makeEitherLoc root es1)+                           (makeEitherLoc root es2)+        makeDef (DefinitionWiredIn text) =+          DefinitionUseless ("Wired-in: " <> text <> " (nowhere to go!)")+        makeDef (DefinitionBinder text) =+          DefinitionUseless ("Binder: " <> text <> " (you're already there!)")++-- | Print a package:module pair.+makeModuleId :: FayModuleId -> ModuleId+makeModuleId (FayModuleId _ mname pkg) =+  (ModuleId (packageName pkg) (unFayModuleName mname))++-- | Get type info of span.+typeInfo :: Handle+         -> FayProjectId -> String+         -> Int -> Int+         -> Int -> Int+         -> Server ()+typeInfo h fpid filename sl sc el ec = do+  getTypeInfo span 0 fpid+  wait fpid+       (Callback+          (\_ msg -> do+             case msg of+               SubExprsResults span' infos+                 | span' == span -> do reply h (ReplyTypeInfo (map toSpanType (concat infos)))+                                       return Done+               _ -> return NotDone))++  where span =+          SourceSpan (FayFileName (pack filename))+                     sl sc+                     el ec++-- | Save the given module.+saveTheModule :: Handle -> FayProjectId -> FilePath -> FilePath -> Server ()+saveTheModule h fpid root filename =+  withTokens (\tokensVar -> do+    let fname = FayFileName (pack filename)+    token <- getToken tokensVar fpid root filename+    text <- io (T.readFile (root </> filename))+    catch (do SaveFileOutput token' _ <- saveFile fname text token fpid+              updateToken tokensVar root filename token'+              reply h (ReplySaveStatus False))+          -- A command exception will be thrown when the file is out of+          -- date. So we just immediately grab the new version of the+          -- file, overwrite out local copy. Emacs will prompt the user+          -- about it at the right time.+          (\(_ :: CommandException) ->+             do updateFileContents tokensVar fpid root filename+                reply h (ReplySaveStatus True)))++-- | Check the given module. Necessary for flycheck.+checkModule :: Handle -> FayProjectId -> FilePath -> FilePath -> FilePath -> Server ()+checkModule h fpid root filename filepath =+  withTokens (\tokensVar -> do+    let fname = FayFileName (pack filename)+    token <- getToken tokensVar fpid root filename+    text <- io (T.readFile filepath)+    result <- try (saveFile fname text token fpid)+    case result of+      -- If there's an out of date error, don't break the flychecker,+      -- just return no errors for now.+      Left (_ :: CommandException) -> reply h (ReplyCompileInfos [])+      Right (SaveFileOutput token' (CompileChanged mcid _)) -> do+        updateToken tokensVar root filename token'+        case mcid of+          Nothing -> reply h (ReplyCompileInfos [])+          Just _ -> do+            wait fpid+                 (Callback+                    (\_ msg ->+                       case msg of+                         StatusSnapshot snapshot _ ->+                           case snapCompileStatus snapshot of+                             RunnerNotCompiling  -> return NotDone+                             RunnerCompiling _ _ -> return NotDone+                             RunnerCompileDone _ infos -> do+                               reply h+                                     (ReplyCompileInfos infos)+                               return Done+                         _ -> return NotDone)))++-- The communication API++-- | Reply with the given value.+reply :: (ToJSON a,Show a) => Handle -> a -> Server ()+reply h r = do+  $(logDebug) ("-> " <> ellipsize 140 (pack (show r)))+  io (L8.hPutStrLn h (encode r))++-- | Close the given handle.+close :: Handle -> Server ()+close h = do+  $(logDebug) ("Connection closed to " <> pack (show h))+  io (hClose h)++-- | Start poller if there isn't already one running for the given+-- project, and in any case add the given callback to the list. This+-- blocks on the result.+wait :: FayProjectId -- ^ The project to poll on.+     -> Callback     -- ^ Take the message or pass it back.+     -> Server ()+wait fpid (Callback callback) = do+  waiter <- newEmptyMVar+  let wcallback = waiting waiter+  psvar <- asks serverPollers+  start <- modifyMVar psvar $ \pollers ->+    case M.lookup fpid pollers of+      -- Either insert or append+      Just{}  -> return (M.insertWith (++) fpid [wcallback] pollers,False)+      Nothing -> return (M.insert fpid [wcallback] pollers,True)+  when start $+    void $ fork $ poll fpid+  takeMVar waiter++  where waiting waiter = Callback $ \mid msg ->+          do ret <- callback mid msg+             case ret of+               Done -> putMVar waiter ()+               _ -> return ()+             return ret++-- | Remove a poller from the polling list and any broadcast+-- callbacks.+removePoller :: FayProjectId -> Server ()+removePoller fpid = do+  psvar <- asks serverPollers+  modifyMVar_ psvar (return . M.delete fpid)++-- | Poll for new messages and apply any queued callbacks to them.+poll :: FayProjectId -> Server ()+poll fpid = do+  getIPI fpid+  $(logDebug) ("Polling on project " <> pack (show fpid))+  go PMRImmediateStatusNoMessages++  where go statusHash = do+          result <- try (getProjectMessages statusHash fpid)+          case result of+            Left (SomeException ge) ->+              case fmap Left (cast ge) <|> fmap Right (cast ge) of+                Nothing ->+                  do removePoller fpid+                     throw ge+                Just (e :: Either HttpException CommandException) ->+                  do $(logError) ("Error while polling for messages: " <> pack (show e))+                     $(logError) ("Waiting 10 seconds before polling again ...")+                     threadDelay (1000 * 1000 * 10)+                     go PMRImmediateStatusNoMessages+            Right (ProjectMessagesOutput nextfilt messages) ->+              do psvar <- asks serverPollers+                 modifyMVar_ psvar $ \pollers ->+                   case M.lookup fpid pollers of+                     Nothing -> return pollers+                     Just callbacks -> do+                       callbacks' <- foldM applyCallbacks callbacks messages+                       return (M.insert fpid callbacks' pollers)+                 go (newRequest nextfilt messages)+        newRequest nextfilt messages =+          fromMaybe (if null messages+                        then PMRImmediateStatusNoMessages+                        else PMRImmediateStatusWithMessages nextfilt)+                    (latestHash messages)++        latestHash = listToMaybe . mapMaybe latest . reverse where+          latest (_,StatusSnapshot _ hash) = Just (PMRNextStatusWithMessages PMFilterAll hash)+          latest _ = Nothing++-- | Get initial project information.+getIPI :: FayProjectId -> Server ()+getIPI fpid = do+  _ <- getInitialProjectInfo fpid+  return ()++-- | Applies the given callbacks to the given message. Returns a new+-- list of callbacks. If any of the callbacks are now done, they will+-- be removed from the list.+applyCallbacks :: [Callback] -> (Maybe Int, RunnerMessage) -> Server [Callback]+applyCallbacks callbacks (mtag,msg) = do+  fmap catMaybes $ forM callbacks $ \callback@(Callback call) -> do+    result <- try (call mtag msg)+    case result of+      Right Done -> return Nothing+      Right NotDone -> return (Just callback)+      -- Callbacks that throw exceptions are discarded.+      Left (e :: SomeException) -> do+        $(logError) ("Callback threw exception: " <> pack (show e))+        return Nothing++-- Misc++-- | Get the token of the given file.+getToken :: MVar (Map FilePath FayTutorialToken) -> FayProjectId -> FilePath -> FilePath -> Server FayTutorialToken+getToken tokensVar fpid root file = do+  modifyMVar tokensVar+             (\tokens ->+                case M.lookup key tokens of+                  Nothing -> do token <- getFileToken (FayFileName (T.pack file)) fpid+                                return (M.insert key token tokens,token)+                  Just token -> return (tokens,token))++  where key = root </> file++-- | Update the contents of the given file from the server.+updateFileContents :: MVar (Map FilePath FayTutorialToken) -> FayProjectId -> FilePath -> FilePath+                   -> Server ()+updateFileContents tokensVar fpid root filename = do+  FileContent text token <- getFile (FayFileName (T.pack filename)) fpid+  updateToken tokensVar root filename token+  io (T.writeFile (root </> filename)+                  (fromMaybe "" text))++-- | Update the token of the given file.+updateToken :: MVar (Map FilePath FayTutorialToken) -> FilePath -> FilePath+            -> FayTutorialToken -> Server ()+updateToken tokensVar root file token = do+  $(logDebug) ("Updating file token: " <> T.pack key <> ": " <> pack (show token))+  modifyMVar_ tokensVar+              (return . M.insert key token)++  where key = root </> file++-- | Do something exclusively with tokens. Due to the fact that+-- flychecking and buffer saving *can* occur simultaneously from+-- Emacs, we don't want those two racing save capabilities.+--+-- On the other hand it doesn't matter what order they occur because+-- they'll be saving the same content. So we simply require that any+-- command that uses module tokens needs to happen in an exclusion.+withTokens :: (MVar (Map FilePath FayTutorialToken) -> Server a) -> Server a+withTokens cont = do+  tokensVar <- asks serverTokens+  withMVar tokensVar cont++-- | Get the project ID from either a URL or a project ID.+getFayProjectId :: Either Text FayProjectId -> Server FayProjectId+getFayProjectId = either getProjectId return++-- | Strip the trailing slash.+stripSlash :: String -> String+stripSlash = reverse . dropWhile (=='/') . reverse++-- | Convert an API message to a more structurally convenient reply message.+convertMsg :: FilePath -> SourceInfo -> CompileMessage+convertMsg root SourceInfo{..} =+  CompileMessage (printEitherSpan root infoSpan)+                 kind+                 infoMsg++  where kind =+          case infoKind of+            KindError   -> "error"+            KindWarning -> "warning"+            KindHint    -> "hint"
+ src/library/FP/Server/Config.hs view
@@ -0,0 +1,11 @@+-- | Server configuration.++module FP.Server.Config where++-- | Default url to accept commands on.+defaultUrl :: String+defaultUrl = "https://www.fpcomplete.com/"++-- | Default port to accept commands on.+defaultPort :: Integer+defaultPort = 1990
+ src/library/FP/Server/Spans.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE OverloadedStrings   #-}++-- | Source spans and locations.++module FP.Server.Spans where++import           FP.API.Types+import           FP.Server.Types++import           Data.Maybe+import           Data.Monoid+import           Data.Text                  (Text,unpack)+import qualified Data.Text                  as T+import           Prelude                    hiding (span)+import           System.FilePath++-- | Print either a useful span or a useless one.+printEitherSpan :: FilePath -> EitherSpan -> Text+printEitherSpan _ (TextSpan s) = T.pack $ s+printEitherSpan root (ProperSpan span') = printSourceSpan root span'++-- | Print a span as Main.hs:1:23+printSourceSpan :: FilePath -> SourceSpan -> Text+printSourceSpan root (SourceSpan source fromLine fromCol _toLine _toCol) =+  T.pack (root </> T.unpack (unFayFileName source)) <> ":" <>+  T.pack (show fromLine) <> ":" <>+  T.pack (show fromCol)++toSpanType :: TypeInfo -> SpanType+toSpanType (TypeInfo (SourceSpan _ sl sc el ec) srcstr typ) =+  SpanType sl sc el ec srcstr typ++makeEitherLoc :: FilePath -> EitherSpan -> Maybe Loc+makeEitherLoc _ TextSpan{} = Nothing+makeEitherLoc root (ProperSpan span) = Just (makeLoc root span)++makeLoc :: FilePath -> SourceSpan -> Loc+makeLoc root (SourceSpan source fl fc tl tc) =+  Loc (root </> unpack (unFayFileName source))+      fl fc+      tl tc
+ src/library/FP/Server/Types.hs view
@@ -0,0 +1,185 @@+{-# OPTIONS -fno-warn-orphans #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeSynonymInstances #-}++-- | Server types.++module FP.Server.Types where++import FP.API.Run+import FP.API.Types++import Control.Concurrent (MVar)+import Control.Monad.Logger+import Control.Monad.Reader+import Data.Aeson+import Data.Default+import Data.Map (Map)+import Data.Maybe+import Data.Text as T+import GHC.Generics++-- | Server monad.+type Server = LoggingT (ReaderT ServerReader IO)++-- | The configuration for the server and some state.+data ServerReader = ServerReader+  { serverCC :: ClientConfig+  , serverConfig :: Config+  , serverPollers :: MVar (Map FayProjectId [Callback])+  , serverTokens :: MVar (MVar (Map FilePath FayTutorialToken))+  }++-- | A callback that will look at incoming messages and determine+-- whether it's what it wants.+newtype Callback = Callback (Maybe Int -> RunnerMessage -> Server Done)++-- | Is the callback done looking for what it wants?+data Done = NotDone | Done+  deriving Eq++-- | Necessary for the pollers map.+deriving instance Ord FayProjectId++-- | Necessary for calling Fay API functions.+instance MonadClient Server where+  getClientConfig = asks serverCC++-- | Configuration for server.+data Config = Config+  { configToken :: !String+  , configUrl   :: !String+  , configPort  :: !Integer+  , configAgent :: !Text+  , configDebug :: !Bool+  , configStartServer :: !Bool+  } deriving (Show)++instance Default Config where+  def = Config "" "https://fpcomplete.com" 1990 "fpco-api" True False++-- | Message from the client.+data Msg = MsgSaveModule FayProjectId FilePath FilePath+         | MsgCheckModule FayProjectId FilePath FilePath FilePath+         | MsgTypeInfo FayProjectId FilePath Int Int Int Int+         | MsgGetDefinition FayProjectId FilePath FilePath Int Int Int Int+         | MsgAutoComplete FayProjectId FilePath Text+         | MsgHoogleIdent FayProjectId FilePath Text+         | MsgHoogleDb FayProjectId Text+         | MsgDownloadFiles (Either Text FayProjectId) FilePath+         | MsgWriteEmacsConfig (Either Text FayProjectId) FilePath+  deriving (Generic,Show)++instance FromJSON Msg+instance ToJSON Msg++-- | Reply to the client.+data Reply = ReplyPong ()+           | ReplyOK ()+           | ReplyCompileMessages [CompileMessage]+           | ReplyCompileInfos [SourceInfo]+           | ReplyTypeInfo [SpanType]+           | ReplyLocation DefinitionLoc+           | ReplyCompletions [Text]+           | ReplyHoogleResults [HoogleResult]+           | ReplyHoogleResult HoogleResult+           | ReplySaveStatus Bool+ deriving (Generic,Show)++instance ToJSON Reply+instance FromJSON Reply++data DefinitionLoc = DefinitionLoc Loc+                   | DefinitionUseless Text+                   | DefinitionImport Text        -- Dunno+                                      ModuleId    -- Package+                                      ModuleId    -- Package 2.0 (?)+                                      (Maybe Loc) -- Span+                                      (Maybe Loc) -- Span 2.0+  deriving (Generic,Show)++instance ToJSON DefinitionLoc+instance FromJSON DefinitionLoc++data ModuleId = ModuleId Text Text+  deriving (Generic,Show)++instance ToJSON ModuleId+instance FromJSON ModuleId++data Loc = Loc FilePath Int Int Int Int+  deriving (Generic,Show)++instance ToJSON Loc+instance FromJSON Loc++-- | A type info thing.+data SpanType = SpanType Int Int Int Int -- Position+                         Text            -- Source string+                         Text            -- Type+  deriving (Generic,Show)++instance ToJSON SpanType+instance FromJSON SpanType++-- | A message from the compiler about code.+data CompileMessage+  = CompileMessage Text Text Text+   deriving (Show,Generic)++instance ToJSON CompileMessage+instance FromJSON CompileMessage++instance FromJSON FayProjectId where+  parseJSON v = fmap (FayProjectId . T.pack . (show :: Int -> String))+                     (parseJSON v)++instance ToJSON FayProjectId where+  toJSON (FayProjectId i) =+    toJSON ((fromMaybe (error ("Unable to read: " ++ show i)) .+             fmap fst .+             listToMaybe)+              (reads (T.unpack i) :: [(Int,String)]))++-- Only for communication between fpco-client and fpco-server. These+-- types would not be nice to handle outside of Haskell.++deriving instance Generic SourceInfo+instance ToJSON SourceInfo+instance FromJSON SourceInfo++deriving instance Generic SourceInfoKind+instance ToJSON SourceInfoKind+instance FromJSON SourceInfoKind++deriving instance Generic EitherSpan+instance ToJSON EitherSpan+instance FromJSON EitherSpan++deriving instance Generic SourceSpan+instance ToJSON SourceSpan+instance FromJSON SourceSpan++deriving instance Generic FayFileName+instance ToJSON FayFileName+instance FromJSON FayFileName++deriving instance Generic FayModuleName+instance ToJSON FayModuleName+instance FromJSON FayModuleName++deriving instance Generic HoogleResult+instance ToJSON HoogleResult+instance FromJSON HoogleResult++deriving instance Generic PackageLink+instance ToJSON PackageLink+instance FromJSON PackageLink++deriving instance Generic ModuleLink+instance ToJSON ModuleLink+instance FromJSON ModuleLink