packages feed

cornelis-0.2.0.0: src/Cornelis/Diff.hs

-- | Maintain a Diff between the text that Agda loaded most recently
-- and the current Vim buffer. There is one such diff for every buffer,
-- indexed by 'BufferNum'.
--
-- This exports three functions:
-- - 'resetDiff' empties the diff when reloading Agda
-- - 'recordUpdate' adds a buffer update to the diff.
-- - 'translateInterval' applies the Diff to an interval coming from Agda,
--   turning it into an interval for the current buffer.
module Cornelis.Diff
  ( resetDiff
  , translateInterval
  , recordUpdate
  , Replace(..)
  , Colline(..)
  , Vallee(..)
  ) where

import Data.IORef (atomicModifyIORef')
import qualified Data.Map as Map
import Data.Tuple (swap)
import DiffLoc (Replace(..), Colline(..), Vallee(..))
import qualified DiffLoc as D
import Neovim
import Cornelis.Offsets
import Cornelis.Types

-- | A general function for modifying Diffs, shared between the three functions below.
modifyDiff :: BufferNum -> (Diff0 -> (Diff0, a)) -> Neovim CornelisEnv a
modifyDiff buf f = do
  csRef <- asks ce_state
  liftIO $ atomicModifyIORef' csRef $ \cs ->
    let (a, ds') = Map.alterF (fmap Just . swap . alter) buf (cs_diff cs)
    in (cs { cs_diff = ds' }, a)
  where
    alter Nothing = f D.emptyDiff
    alter (Just d) = f d

-- These three functions are essentially monadic wrappers, using 'modifyDiff',
-- around the corresponding diff-loc functions:
--
-- @
-- 'D.emptyDiff' :: Diff0
-- 'D.addDiff' :: D.Replace DPos -> Diff0 -> Diff0
-- 'D.mapDiff' :: Diff0 -> D.Interval DPos -> Maybe (D.Interval DPos)
-- @
--
-- This relies on instances of 'D.Amor' and 'D.Origin' implemented in 'Cornelis.Offsets'.

-- | Reset the diff to an empty diff.
resetDiff :: BufferNum -> Neovim CornelisEnv ()
resetDiff buf = modifyDiff buf $ const (D.emptyDiff, ())

-- | Add a buffer update (insertion or deletion) to the diff.
-- The buffer update event coming from Vim is structured exactly how the diff-loc
-- library expects it.
recordUpdate :: BufferNum -> D.Replace DPos -> Neovim CornelisEnv ()
recordUpdate buf r = modifyDiff buf $ \d -> (D.addDiff r d, ())

-- | Given an interval coming from Agda (a pair of start and end positions),
-- find the corresponding interval in the current buffer.
-- If an edit touches the interval, return Nothing.
translateInterval :: BufferNum -> Interval VimPos -> Neovim CornelisEnv (Maybe (Interval VimPos))
translateInterval buf sp = modifyDiff buf $ \d -> (d, translate d)
  where
    translate :: Diff0 -> Maybe (Interval VimPos)
    translate d = fmap toInterval . D.mapDiff d =<< fromInterval sp

-- | Convert a Cornelis interval (pair of positions) to a diff-loc interval
-- (start position and a vector towards the end position). This is Nothing
-- if the end precedes the start.
fromInterval :: Interval VimPos -> Maybe (D.Interval DPos)
fromInterval (Interval p1 p2) = (q1 D.:..) <$> (q2 D..-.? q1)
  where
    q1 = fromVimPos p1
    q2 = fromVimPos p2

-- | Inverse of 'fromInterval'.
toInterval :: D.Interval DPos -> Interval VimPos
toInterval (q D.:.. v) = Interval (toVimPos q) (toVimPos (q D..+ v))

-- | Convert from Cornelis positions to diff-loc positions.
fromVimPos :: VimPos -> DPos
fromVimPos (Pos l c) = Colline l c

toVimPos :: DPos -> VimPos
toVimPos (Colline l c) = Pos l c