packages feed

cornelis-0.2.0.0: src/Cornelis/Vim.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}

module Cornelis.Vim where

import           Control.Lens ((%~), _head, _last, (&))
import           Cornelis.Offsets
import           Cornelis.Types
import           Cornelis.Utils (objectToInt, savingCurrentPosition, savingCurrentWindow)
import           Data.Foldable (toList)
import           Data.Int
import qualified Data.Map as M
import qualified Data.Text as T
import           Data.Text.Encoding (encodeUtf8)
import qualified Data.Vector as V
import           Neovim
import           Neovim.API.Text


vimFirstLine :: Int64
vimFirstLine = 0

vimLastLine :: Int64
vimLastLine = -1

getWindowCursor :: Window -> Neovim env AgdaPos
getWindowCursor w = do
  (toOneIndexed -> row, toZeroIndexed -> col) <- window_get_cursor w
  -- window_get_cursor gives us a 1-indexed line, but that is the same way that
  -- lines are indexed.
  let line = zeroIndex row
  b <- window_get_buffer w
  unvimify b (Pos line col)

-- | TODO(sandy): POSSIBLE BUG HERE. MAKE SURE YOU SET THE CURRENT WINDOW
-- BEFORE CALLING THIS FUNCTION
getpos :: Buffer -> Char -> Neovim env AgdaPos
getpos b mark = do
  -- getpos gives us a (1,1)-indexed position!
  ObjectArray [_, objectToInt @Int -> Just (toOneIndexed -> line), objectToInt @Int -> Just (toOneIndexed -> col), _]
    <- vim_call_function "getpos" $ V.fromList [ObjectString $ encodeUtf8 $ T.singleton mark]
  unvimify b (Pos (zeroIndex line) (zeroIndex col))

data SearchMode = Forward | Backward
  deriving (Eq, Ord, Show)

searchpos :: Buffer -> [Text] -> SearchMode -> Neovim env AgdaPos
searchpos b pats dir = do
  -- unlike getpos, these columns are 0 indexed W T F
  ObjectArray [objectToInt @Int -> Just (toOneIndexed -> line), objectToInt @Int -> Just (toZeroIndexed -> col)]
    <- vim_call_function "searchpos" $ V.fromList
        [ ObjectString $ encodeUtf8 $ T.intercalate "\\|" pats
        , ObjectString $ encodeUtf8 $ case dir of
            Forward -> "n"
            Backward -> "bn"
        ]
  unvimify b (Pos (zeroIndex line) col)

setWindowCursor :: Window -> AgdaPos -> Neovim env ()
setWindowCursor w p = do
  b <- window_get_buffer w
  Pos l c <- vimify b p
  window_set_cursor w (fromOneIndexed (oneIndex l), fromZeroIndexed c)

replaceInterval :: Buffer -> Interval AgdaPos -> Text -> Neovim env ()
replaceInterval b ival str
  = do
    Interval (Pos sl sc) (Pos el ec) <- traverse (vimify b) ival
    nvim_buf_set_text b (from0 sl) (from0 sc) (from0 el) (from0 ec) $ V.fromList $ T.lines str
  where
    from0 = fromZeroIndexed

------------------------------------------------------------------------------
-- | Vim insists on returning byte-based offsets for the cursor positions...
-- why the fuck? This function undoes the problem.
unvimify :: Buffer -> VimPos -> Neovim env AgdaPos
unvimify b (Pos line col) = do
  txt <- getBufferLine b line
  let col' = fromBytes txt col
  pure (Pos (oneIndex line) (oneIndex col'))

vimify :: Buffer -> AgdaPos -> Neovim env VimPos
vimify b (Pos (zeroIndex -> line) (zeroIndex -> col)) = do
  txt <- getBufferLine b line
  let col' = toBytes txt col
  pure (Pos line col')

getIndent :: Buffer -> LineNumber 'ZeroIndexed -> Neovim env Int
getIndent b l = do
  txt <- getBufferLine b l
  pure $ T.length $ T.takeWhile (== ' ') txt


getBufferLine :: Buffer -> LineNumber 'ZeroIndexed -> Neovim env Text
getBufferLine b l = buffer_get_line b (fromZeroIndexed l)

getBufferInterval :: Buffer -> Interval AgdaPos -> Neovim env Text
getBufferInterval b (Interval start end) = do
    Pos sl _ <- vimify b start
    Pos el _ <- vimify b end
    -- nvim_buf_get_lines is exclusive in its end line, thus the plus 1
    ls <- fmap toList $ nvim_buf_get_lines b (from0 sl) (from0 el + 1) False
    pure $ T.unlines $
      ls & _last %~ T.take (from1 (p_col end))
         & _head %~ T.drop (from1 (p_col start))
  where
    from0 = fromZeroIndexed
    from1 = fromZeroIndexed . zeroIndex  -- add 1 to a one-indexed arg before passing it to take/drop

reportError :: Text -> Neovim env ()
reportError = vim_report_error

reportInfo :: Text -> Neovim env ()
reportInfo m = vim_out_write $ m <> "\n"

setreg :: Text -> Text -> Neovim env ()
setreg reg val
  = void
  $ vim_call_function "setreg"
  $ V.fromList
    [ ObjectString $ encodeUtf8 reg
    , ObjectString $ encodeUtf8 val
    ]

getExtmarkIntervalById :: Int64 -> Buffer -> Extmark -> Neovim env (Maybe AgdaInterval)
getExtmarkIntervalById ns b (Extmark x) = do
  res
    <- nvim_call_function "nvim_buf_get_extmark_by_id"
     $ V.fromList
     $ b +: ns +: x +: M.singleton @Text "details" True +: []
  case res of
    ObjectArray [ objectToInt @Int -> Just (toZeroIndexed -> sline)
                , objectToInt @Int -> Just (toZeroIndexed -> scol)
                , ObjectMap details
                ] -> do
      let toZ = fmap toZeroIndexed . objectToInt @Int
          Just eline = toZ $ details M.! ObjectString "end_row"
          Just ecol  = toZ $ details M.! ObjectString "end_col"
      fmap Just $ traverse (unvimify b) $ Interval (Pos sline scol) $ Pos eline ecol
    _ -> pure Nothing

------------------------------------------------------------------------------
-- | Awful function that does the motion in visual mode and gives you back
-- where vim thinks the @'<@ and @'>@ marks are.
--
-- I'm so sorry.
getSurroundingMotion
    :: Window
    -> Buffer
    -> Text
    -> AgdaPos
    -> Neovim env (AgdaPos, AgdaPos)
getSurroundingMotion w b motion p = do
  savingCurrentWindow $ do
    savingCurrentPosition w $ do
      nvim_set_current_win w
      setWindowCursor w p
      vim_command $ "normal v" <> motion
      start <- getpos b 'v'
      end <- getpos b '.'
      void $ nvim_input "<esc>"
      pure (start, end)

------------------------------------------------------------------------------
-- | Get an interval to replace for a lambda case split
getLambdaClause
    :: Window
    -> Buffer
    -> AgdaInterval
    -> Neovim env AgdaInterval
getLambdaClause w b (Interval p0 p1) = do
  savingCurrentWindow $ do
    savingCurrentPosition w $ do
      nvim_set_current_win w
      setWindowCursor w p0
      start <- searchpos b ["{", ";"] Backward
      setWindowCursor w p1
      end <- searchpos b [";", "}"] Forward
      pure (Interval start end)