cornelis-0.2.0.0: src/Cornelis/Goals.hs
{-# LANGUAGE OverloadedStrings #-}
module Cornelis.Goals where
import Control.Arrow ((&&&))
import Control.Lens
import Cornelis.Agda (withAgda)
import Cornelis.Offsets
import Cornelis.Types
import Cornelis.Utils
import Cornelis.Vim
import Data.Foldable (toList, fold)
import Data.List
import qualified Data.Map as M
import Data.Maybe
import Data.Ord
import qualified Data.Text as T
import Data.Traversable (for)
import Neovim
import Neovim.API.Text
import Neovim.User.Input (input)
------------------------------------------------------------------------------
-- | Get the spanning interval of an interaction point. If we already have
-- highlighting information from vim, use the extmark for this goal, otherwise
-- using the interval that Agda knows about.
getIpInterval :: Buffer -> InteractionPoint Identity -> Neovim CornelisEnv AgdaInterval
getIpInterval b ip = do
ns <- asks ce_namespace
extmap <- withBufferStuff b $ pure . bs_ip_exts
fmap (fromMaybe (ip_interval' ip)) $
case flip M.lookup extmap $ ip_id ip of
Just i -> getExtmarkIntervalById ns b i
Nothing -> pure Nothing
--------------------------------------------------------------------------------
-- | Move the vim cursor to a goal in the current window
findGoal :: Ord a => (AgdaPos -> AgdaPos -> Maybe a) -> Neovim CornelisEnv ()
findGoal hunt = withAgda $ do
w <- vim_get_current_window
b <- window_get_buffer w
withBufferStuff b $ \bs -> do
pos <- getWindowCursor w
let goals = toList $ bs_ips bs
judged_goals <- fmap catMaybes $ for goals $ \ip -> do
int <- getIpInterval b ip
pure
. sequenceA
. (id &&& hunt pos)
$ iStart int
case judged_goals of
[] -> reportInfo "No hole matching predicate"
_ -> do
let pos' = fst $ maximumBy (comparing snd) judged_goals
setWindowCursor w pos'
------------------------------------------------------------------------------
-- | Move the vim cursor to the previous interaction point.
prevGoal :: Neovim CornelisEnv ()
prevGoal =
findGoal $ \pos goal ->
case pos > goal of
False -> Nothing
True -> Just ( p_line goal .-. p_line pos
, p_col goal .-. p_col pos -- TODO: This formula looks fishy
)
------------------------------------------------------------------------------
-- | Move the vim cursor to the next interaction point.
nextGoal :: Neovim CornelisEnv ()
nextGoal =
findGoal $ \pos goal ->
case pos < goal of
False -> Nothing
True -> Just $ Down ( p_line goal .-. p_line pos
, p_col goal .-. p_col pos
)
------------------------------------------------------------------------------
-- | Uses highlighting extmarks to determine what a hole is; since the user
-- might have typed inside of a {! !} goal since they last saved.
getGoalAtCursor :: Neovim CornelisEnv (Buffer, Maybe (InteractionPoint Identity))
getGoalAtCursor = do
w <- nvim_get_current_win
b <- window_get_buffer w
p <- getWindowCursor w
fmap (b, ) $ getGoalAtPos b p
getGoalAtPos
:: Buffer
-> AgdaPos
-> Neovim CornelisEnv (Maybe (InteractionPoint Identity))
getGoalAtPos b p = do
fmap (getFirst . fold) $ withBufferStuff b $ \bs -> do
for (bs_ips bs) $ \ip -> do
int <- getIpInterval b ip
pure $ case containsPoint int p of
False -> mempty
True -> pure $ ip { ip_intervalM = Identity int }
------------------------------------------------------------------------------
-- | Run a continuation on a goal at the current position in the current
-- buffer, if it exists.
withGoalAtCursor
:: (Buffer -> InteractionPoint Identity -> Neovim CornelisEnv a)
-> Neovim CornelisEnv (Maybe a)
withGoalAtCursor f = getGoalAtCursor >>= \case
(_, Nothing) -> do
reportInfo "No goal at cursor"
pure Nothing
(b, Just ip) -> fmap Just $ f b ip
------------------------------------------------------------------------------
-- | Run the first continuation on the goal at the current position,
-- otherwise run the second continuation.
--
-- If there is a non-empty hole, provide its content to the first continuation.
-- If the hole is empty, prompt for input and provide that.
--
-- If there is no goal, prompt the user for input and run the second continuation.
withGoalContentsOrPrompt
:: String
-- ^ Text to print when prompting the user for input
-> (InteractionPoint Identity -> String -> Neovim CornelisEnv a)
-- ^ Continuation to run on goal, with hole contents
-> (String -> Neovim CornelisEnv a)
-- ^ Continuation to run on user input if there's no goal here
-> Neovim CornelisEnv a
withGoalContentsOrPrompt prompt_str on_goal on_no_goal = getGoalAtCursor >>= \case
(_, Nothing) ->
-- If there's no goal here, run `on_no_goal` on user input.
prompt >>= on_no_goal
(b, Just ip) -> do
content <- getGoalContents b ip
-- If there is a goal under the cursor, but it's contents
-- are empty, prompt the user for input. Otherwise, unpack
-- the hole contents are provide that.
if T.null content
then prompt >>= on_goal ip
else on_goal ip (T.unpack content)
where
prompt = input prompt_str Nothing Nothing
------------------------------------------------------------------------------
-- | Get the contents of a goal.
getGoalContentsMaybe :: Buffer -> InteractionPoint Identity -> Neovim CornelisEnv (Maybe Text)
getGoalContentsMaybe b ip = do
int <- getIpInterval b ip
iv <- fmap T.strip $ getBufferInterval b int
pure $ case iv of
"?" -> Nothing
-- Chop off {!, !} and trim any spaces.
_ -> Just $ T.strip $ T.dropEnd 2 $ T.drop 2 iv
------------------------------------------------------------------------------
-- | Like 'getGoalContents_maybe'.
getGoalContents :: Buffer -> InteractionPoint Identity -> Neovim CornelisEnv Text
getGoalContents b ip = fromMaybe "" <$> getGoalContentsMaybe b ip
------------------------------------------------------------------------------
-- | Replace all single @?@ tokens with interaction holes.
replaceQuestion :: Text -> Text
replaceQuestion = T.unwords . fmap go . T.words
where
go "(?" = "({! !}"
go "?" = "{! !}"
go x =
case T.dropWhileEnd (== ')') x of
"?" -> "{! !}" <> T.drop 1 x
_ -> x