packages feed

hsdev-0.1.4.1: src/Data/Mark.hs

{-# LANGUAGE ViewPatterns, OverloadedStrings, GeneralizedNewtypeDeriving, TypeSynonymInstances, FlexibleInstances, RankNTypes, MultiParamTypeClasses #-}

module Data.Mark (
	Point(..), point, lineStart, (.-.), (.+.), till, Size, linesSize, stringSize,
	Range(..), rangeLines, emptyRange,
	line,
	range, rangeSize, expandLines, at,
	-- * Mappings
	Map(..), apply, back,
	cut, insert,
	cutRange, insertRange,
	-- * Edited data
	Contents, Edit(..),
	EditM(..), editRange, mapRange, runEdit, edit, editEval,
	Prefix(..), prefix, Suffix(..), suffix, concatCts, splitCts,
	-- * Editable class
	Editable(..), measure,
	-- * Actions
	EditAction(..),
	Replace(..), inverse, reverseEdits, run
	) where

import Prelude hiding (splitAt, length, lines, unlines)

import Control.Arrow ((&&&))
import Control.Lens (view)
import Control.Lens.Iso
import Control.Monad.State
import Data.Aeson
import qualified Data.List as List (splitAt, length, break, intercalate)
import Data.Text (Text)
import qualified Data.Text as T (splitAt, length, split, intercalate)

import HsDev.Util ((.::))

-- | Point at text: line and column
data Point = Point {
	pointLine :: Int,
	pointColumn :: Int }
		deriving (Eq, Ord, Read, Show)

instance ToJSON Point where
	toJSON (Point l c) = object ["line" .= l, "column" .= c]

instance FromJSON Point where
	parseJSON = withObject "point" $ \v -> Point <$> v .:: "line" <*> v .:: "column"

-- | Distance between points is measured in lines and columns.
-- And it is defined, that distance between point at l:c and point (l + 1):0 is one line no matter c is
-- because we need to go to new line to reach destination point
-- Columns are taken into account only if points are on the same line
-- @pt .-. base@ is distance from @base@ to @pt@
-- Distance can't be less then zero lines and columns
(.-.) :: Point -> Point -> Point
(Point l c) .-. (Point bl bc)
	| bl < l = Point (l - bl) c
	| bl == l = Point 0 (max 0 (c - bc))
	| otherwise = Point 0 0

-- | Opposite to ".-.", @(pt .-. base) .+. base = pt@
(.+.) :: Point -> Point -> Point
(Point l c) .+. (Point bl bc)
	| l == 0 = Point bl (c + bc)
	| otherwise = Point (l + bl) c

point :: Int -> Int -> Point
point l c
	| l < 0 = error "Line can't be less then zero"
	| c < 0 = error "Column can't be less then zero"
	| otherwise = Point l c

lineStart :: Int -> Point
lineStart l = point l 0

-- | Range from "Point" to "Point"
data Range = Range {
	rangeFrom :: Point,
	rangeTo :: Point }
		deriving (Eq, Ord, Read, Show)

type Size = Point

instance Monoid Size where
	mempty = Point 0 0
	l `mappend` r = r .+. l

rangeLength :: Range -> Size
rangeLength (Range f t) = t .-. f

-- | Range from one @Point@ to another
till :: Point -> Point -> Range
l `till` r = Range (min l r) (max l r)

-- | Distance in @n@ lines
linesSize :: Int -> Point
linesSize n = Point n 0

-- | Distance in @n@ chars within one line
stringSize :: Int -> Point
stringSize = Point 0

instance ToJSON Range where
	toJSON (Range f t) = object ["from" .= f, "to" .= t]

instance FromJSON Range where
	parseJSON = withObject "range" $ \v -> Range <$> v .:: "from" <*> v .:: "to"

-- | "Range" height in lines. Any "Range" at least of one line height
rangeLines :: Range -> Int
rangeLines r = succ $ pointLine (rangeTo r) - pointLine (rangeFrom r)

-- | Is "Range" empty
emptyRange :: Range -> Bool
emptyRange r = rangeTo r == rangeFrom r

-- | n'th line range, starts at the beginning of line and ends on the next line
line :: Int -> Range
line l = range (Point l 0) (Point (succ l) 0)

-- | Make range
range :: Point -> Point -> Range
range f t = Range (min f t) (max f t)

-- | Make range from starting point and its size
rangeSize :: Point -> Size -> Range
rangeSize pt sz = range pt (sz .+. pt)

-- | Expand range to contain full lines
expandLines :: Range -> Range
expandLines (Range (Point sline _) (Point eline _)) = Range (Point sline 0) (Point (succ eline) 0)

-- | Get contents at specified range
at :: Editable a => Contents a -> Range -> Contents a
at cts r =
	onHead (snd . splitAt (pointColumn $ rangeFrom r)) .
	onLast (fst . splitAt (pointColumn $ rangeTo r)) .
	take (rangeLines r) .
	drop (pointLine (rangeFrom r)) $
	cts
	where
		onHead :: (a -> a) -> [a] -> [a]
		onHead _ [] = []
		onHead f (x:xs) = f x : xs
		onLast :: (a -> a) -> [a] -> [a]
		onLast _ [] = []
		onLast f l@(last -> x) = init l ++ [f x]

-- | Main idea is that there are only two basic actions, that chances ranges: inserting and cutting
-- When something is cutted out or inserted in, range positions must be updated
-- All editings can be represented as many cuts and inserts, so we can combine them to get function
-- which maps source ranges to ranges on updated data
-- Because insert is dual to cut (and therefore composes iso), we can also get function to map ranges back
-- Combining this functions while edit, we get function, that maps ranges from source data to edited one
-- To get back function, we must also combine opposite actions, or we can represent actions as isomorphisms
-- Same idea goes for modifying contents, represent each action as isomorphism and combine them together
newtype Map = Map { mapIso :: Iso' Range Range }

instance Monoid Map where
	mempty = Map $ iso id id
	(Map l) `mappend` (Map r) = Map (l . r)

-- | Apply mapping
apply :: Map -> Range -> Range
apply = view . mapIso

-- | Back mapping
back :: Map -> Map
back (Map f) = Map (from f)

-- | Apply mapping to starting position only
applyStart :: Map -> Range -> Range
applyStart m r = rangeFrom (apply m r) `rangeSize` rangeLength r

-- | Cut range mapping
cut :: Range -> Map
cut rgn = Map $ iso (cutRange rgn) (insertRange rgn)

-- | Opposite to "cut"
insert :: Range -> Map
insert = back . cut

-- | Update second range position as if it was data cutted at first range
cutRange :: Range -> Range -> Range
cutRange (Range is ie) (Range s e) = Range
	(if is < s then (s .-. ie) .+. is else s)
	(if is < e then (e .-. ie) .+. is else e)

-- | Update second range position as if it was data inserted at first range
insertRange :: Range -> Range -> Range
insertRange (Range is ie) (Range s e) = Range
	(if is < s then (s .-. is) .+. ie else s)
	(if is < e then (e .-. is) .+. ie else e)

-- | Contents is list of lines
type Contents a = [a]

-- | Edit data
data Edit a = Edit {
	editCts :: Contents a -> Contents a, -- ^ Edit contents splitted by lines
	editMap :: Map } -- ^ Map range from source contents to edited

instance Monoid (Edit a) where
	mempty = Edit id mempty
	(Edit fl ml) `mappend` (Edit fr mr) = Edit (fr . fl) (ml `mappend` mr)

-- | Edit monad is state on "Edit", it also collects range mappings
newtype EditM s a = EditM { runEditM :: State (Edit s) a }
	deriving (Functor, Applicative, Monad, MonadState (Edit s))

-- | Basic edit action in monad
-- It takes range, range edit function and contents updater
-- and passes mapped range to these functions to get new state
editRange :: Range -> (Range -> Edit a) -> EditM a ()
editRange rgn edit' = do
	rgn' <- mapRange rgn
	modify (`mappend` edit' rgn')

-- | Get mapped range
mapRange :: Range -> EditM a Range
mapRange rgn = gets (($ rgn) . apply . editMap)

-- | Run edit monad
runEdit :: Editable s => EditM s a -> (a, Edit s)
runEdit act = runState (runEditM act) mempty

-- | Edit contents
edit :: Editable s => s -> EditM s a -> s
edit cts = snd . editEval cts

-- | Eval edit
editEval :: Editable s => s -> EditM s a -> (a, s)
editEval cts act = (v, unlines . editCts st . lines $ cts) where
	(v, st) = runEdit act

-- | Prefix of contents cutted at some point
data Prefix a = Prefix {
	prefixLines :: [a],
	prefixLine :: a }
		deriving (Eq, Ord, Read, Show)

instance Functor Prefix where
	fmap f (Prefix ls l) = Prefix (fmap f ls) (f l)

-- | Make prefix from full contents
prefix :: Contents a -> Prefix a
prefix cts = Prefix (init cts) (last cts)

-- | Suffix of contents
data Suffix a = Suffix {
	suffixLine :: a,
	suffixLines :: [a] }
		deriving (Eq, Ord, Read, Show)

instance Functor Suffix where
	fmap f (Suffix l ls) = Suffix (f l) (fmap f ls)

suffix :: Contents a -> Suffix a
suffix cts = Suffix (head cts) (tail cts)

-- | Concat prefix and suffix. First line of suffix is appended to last line of prefix
concatCts :: Monoid a => Prefix a -> Suffix a -> Contents a
concatCts (Prefix ps p) (Suffix s ss) = ps ++ [p `mappend` s] ++ ss

-- | Split contents at point. First argument is function to split one line at position.
splitCts :: Editable a => Point -> Contents a -> (Prefix a, Suffix a)
splitCts (Point l c) cts = (Prefix (take l cts) p, Suffix s (drop (succ l) cts)) where
	(p, s) = splitAt c (cts !! l)

class Monoid a => Editable a where
	splitAt :: Int -> a -> (a, a)
	length :: a -> Int
	lines :: a -> [a]
	unlines :: [a] -> a

instance Editable String where
	splitAt = List.splitAt
	length = List.length
	lines s = case List.break (== '\n') s of
		(pre, "") -> [pre]
		(pre, _:post) -> pre : lines post
	unlines = List.intercalate "\n"

instance Editable Text where
	splitAt = T.splitAt
	length = T.length
	lines = T.split (== '\n')
	unlines = T.intercalate "\n"

-- | Contents size
measure :: Editable s => Contents s -> Size
measure [] = error "Invalid argument"
measure cts = Point (pred $ List.length cts) (length $ last cts)

class EditAction e where
	erase :: Editable s => Range -> e s ()
	write :: Editable s => Point -> s -> e s ()
	replace :: Editable s => Range -> s -> e s ()

instance EditAction EditM where
	erase rgn = editRange rgn (\r -> Edit (erase' r) (cut r)) where
		erase' :: Editable a => Range -> Contents a -> Contents a
		erase' rgn' cts = fst (splitCts (rangeFrom rgn') cts) `concatCts` snd (splitCts (rangeTo rgn') cts)

	write pt cts = editRange (pt `till` pt) (\r -> Edit (write' r) (insert $ rangeFrom r `rangeSize` measure cts')) where
		cts' = lines cts
		write' rgn' origin = prefix (before' `concatCts` suffix cts') `concatCts` after' where
			(before', after') = splitCts (rangeFrom rgn') origin

	replace rgn cts = erase rgn >> write (rangeFrom rgn) cts

-- | Serializable replace action
data Replace s a = Replace {
	replaceRange :: Range,
	replaceWith :: s }
		deriving (Eq, Read, Show)

instance (Editable s, ToJSON s) => ToJSON (Replace s a) where
	toJSON (Replace e c) = object ["range" .= e, "contents" .= c]

instance (Editable s, FromJSON s) => FromJSON (Replace s a) where
	parseJSON = withObject "edit" $ \v -> Replace <$> v .:: "range" <*> v .:: "contents"

instance EditAction Replace where
	erase rgn = Replace rgn mempty
	write pt = Replace (range pt pt)
	replace = Replace

-- | Inverted operation - restores previous state
inverse :: Editable s => Contents s -> Replace s a -> Replace s a
inverse cts (Replace r s) = Replace (rangeFrom r `rangeSize` measure (lines s)) (unlines $ cts `at` r)

-- | Reverse list of operations
reverseEdits :: Editable s => Contents s -> [Replace s ()] -> [Replace s ()]
reverseEdits cts rs = map (remap . inverse cts) $ reverse rs where
	edit' = snd $ runEdit $ run rs
	remap r = r { replaceRange = editMap edit' `applyStart` replaceRange r }

-- | Run replace actions to get monadic action
run :: Editable s => [Replace s ()] -> EditM s ()
run = mapM_ (uncurry replace . (replaceRange &&& replaceWith))