packages feed

yi-0.5.2: Yi/UI/Cocoa/TextStorage.hs

{-# LANGUAGE TemplateHaskell, EmptyDataDecls, MultiParamTypeClasses #-}
--
-- Copyright (c) 2008 Gustav Munkby
--

-- | An implementation of NSTextStorage that uses Yi's FBuffer as
-- the backing store.

module Yi.UI.Cocoa.TextStorage
  ( TextStorage
  , initializeClass_TextStorage
  , newTextStorage
  , setTextStorageBuffer
  , visibleRangeChanged
  ) where

import Prelude (takeWhile, take, dropWhile, drop, span, unzip)
import Yi.Editor (regex, emptyEditor, Editor)
import Yi.Prelude
import Yi.Buffer
import Yi.Style
import Yi.Syntax
import Yi.UI.Cocoa.Utils
import Yi.UI.Utils
import Yi.Window

import Data.Maybe
import qualified Data.Map as M
import qualified Data.List as L

import Foreign hiding (new)
import Foreign.C

import qualified Data.ByteString.Lazy.UTF8 as LB

-- Specify Cocoa imports explicitly, to avoid name-clashes.
-- Since the number of functions recognized by HOC varies
-- between revisions, this seems like the safest choice.
import HOC
import Foundation (
  Unichar,NSString,NSStringClass,NSDictionary,NSRange(..),NSRangePointer,
  length,attributeAtIndexEffectiveRange,attributesAtIndexEffectiveRange,
  attributesAtIndexLongestEffectiveRangeInRange,nsMaxRange,
  beginEditing,endEditing,setAttributesRange,haskellString,
  substringWithRange,initWithStringAttributes,alloc,
  addAttributeValueRange,addAttributesRange)
import AppKit (
  NSTextStorage,NSTextStorageClass,string,fixesAttributesLazily,
  _NSCursor,_NSFont,replaceCharactersInRangeWithString,
  _NSParagraphStyle,defaultParagraphStyle,ibeamCursor,_NSTextStorage,
  editedRangeChangeInLength,nsTextStorageEditedAttributes,
  nsTextStorageEditedCharacters,userFixedPitchFontOfSize)

-- Unfortunately, my version of hoc does not handle typedefs correctly,
-- and thus misses every selector that uses the "unichar" type, even
-- though it has introduced a type alias for it...
$(declareRenamedSelector "characterAtIndex:" "characterAtIndex" [t| CUInt -> IO Unichar |])
instance Has_characterAtIndex (NSString a)
$(declareRenamedSelector "getCharacters:range:" "getCharactersRange" [t| Ptr Unichar -> NSRange -> IO () |])
instance Has_getCharactersRange (NSString a)

-- Introduce a NSString subclass that has a lazy bytestring internally
-- A NSString subclass needs to implement length and characterAtIndex,
-- and for performance reasons getCharactersRange
-- The implementation here is a quick hack and I have no idea how it
-- works with anything except ASCII characters. Cocoa uses UTF16 to
-- store characters, and Yi uses UTF8, so supposedly some recoding
-- has to take place. For UTF8 is converted to Char's that are then
-- just dealt with as if they were in UTF16...

$(declareClass "YiLBString" "NSString")
$(exportClass "YiLBString" "yls_" [
    InstanceVariable "str" [t| LB.ByteString |] [| LB.fromString "" |]
  , InstanceMethod 'length -- '
  , InstanceMethod 'characterAtIndex -- '
  , InstanceMethod 'getCharactersRange -- '
  ])

yls_length :: YiLBString () -> IO CUInt
yls_length slf = do
  -- logPutStrLn $ "Calling yls_length (gah...)"
  slf #. _str >>= return . fromIntegral . LB.length

yls_characterAtIndex :: CUInt -> YiLBString () -> IO Unichar
yls_characterAtIndex i slf = do
  -- logPutStrLn $ "Calling yls_characterAtIndex " ++ show i
  slf #. _str >>= return . last . encodeUTF16 . fromEnum . fst . fromJust . LB.decode . LB.drop (fromIntegral i)

yls_getCharactersRange :: Ptr Unichar -> NSRange -> YiLBString () -> IO ()
yls_getCharactersRange p _r@(NSRange i l) slf = do
  -- logPutStrLn $ "Calling yls_getCharactersRange " ++ show r
  slf #. _str >>=
    pokeArray p .
    concatMap (encodeUTF16 . fromEnum) .
    LB.toString .
    LB.take (fromIntegral l) . -- TODO: Is l given in bytes or characters?
    LB.drop (fromIntegral i)

encodeUTF16 :: Int -> [Unichar]
encodeUTF16 c
  | c < 0x10000 = [fromIntegral c]
  | otherwise   = let c' = c - 0x10000
                  in [0xd800 .|. (fromIntegral $ c' `shiftR` 10),
                      0xdc00 .|. (fromIntegral $ c' .&. 0x3ff)]


-- An implementation of NSTextStorage that uses Yi's FBuffer as
-- the backing store. An implementation must at least implement
-- a O(1) string method and attributesAtIndexEffectiveRange.
-- For performance reasons, attributeAtIndexEffectiveRange is
-- implemented to deal with specific properties such as font.

-- Judging by usage logs, the environment using the text storage
-- seem to rely on strings O(1) behavior and thus caching the
-- result seems like a good idea. In addition attributes are
-- queried for the same location multiple times, and thus caching
-- them as well also seems fruitful.

-- | Use this as the base length of computed stroke ranges
strokeRangeExtent :: Num t => t
strokeRangeExtent = 4000

type PicStroke = (Point, Attributes)
data Picture = Picture
  { picRegion :: Region
  , picStrokes :: [PicStroke]
  }

instance Show Picture where
  show (Picture r ss) = "{{"++show r ++": "++show (take 1 ss)++"@"++show (L.length ss)++"}}"

emptyPicture :: (Picture, NSRange)
emptyPicture = (Picture emptyRegion [], NSRange 0 0)

nullPicture :: Picture -> Bool
nullPicture = null . picStrokes -- Or empty region??

regionEnds :: Region -> (Point, Point)
regionEnds r = (regionStart r, regionEnd r)

dropStrokesWhile :: (PicStroke -> Bool) -> Picture -> Picture
dropStrokesWhile f pic = pic { picRegion = mkRegion nb pe, picStrokes = strokes }
  where 
    (pb, pe) = regionEnds $ picRegion pic
    (nb, strokes) = helper pb (picStrokes pic)
    helper :: Point -> [PicStroke] -> (Point, [PicStroke])
    helper p [] = (p,[])
    helper p ~(x:xs)
      | f x       = helper (fst x) xs
      | otherwise = (p, x:xs)

-- | Extend the currently cached picture, so that it at least
--   covers the desired region. The resulting picture starts
--   at the location of the desired region, but might extend
--   further...
extendPicture :: Region -> (Region -> IO Picture) -> Picture -> IO Picture
extendPicture desired ext cache = do
  -- All possible overlappings of desired and cache regions:
  -- dd   dd  ddd  ddd dddd dd  dd ddd  dd   dd  dd  ddd   dd <- desired
  --   cc  cc  ccc  cc  cc  ccc cc cc  cccc ccc cc  ccc  cc   <- cache
  --  A    B    E   B    A   N  N   E   N    N   A   E    A   <- Get All/Begin/End/None
  -- logPutStrLn $ "extendPicture " ++ show ((db `inRegion` (picRegion cache)), ((de `compare` cb) /= (de `compare` ce)))
  case (
    db `inRegion` (picRegion cache), -- Have start
    de `compare` cb /= de `compare` ce     -- Have end
    ) of 
    ( True,  True) -> return $ dropJunk cache
    ( True, False) -> append (dropJunk cache) <$> ext (mkExtentRegion ce de)
    (False,  True) -> flip append cache       <$> ext (mkRegion db cb)
    (False, False) -> ext (mkExtentRegion db de)
  -- ext (mkExtentRegion db de)
  where
    (db, de) = regionEnds desired
    (cb, ce) = regionEnds $ picRegion cache
    mkExtentRegion b e = mkSizeRegion b (max (b ~- e) strokeRangeExtent)
    dropJunk p = Picture -- Like dropStrokesWhile but always use db as starting point
      { picRegion = mkRegion db (regionEnd $ picRegion p) 
      , picStrokes = dropWhile ((db >=) . fst) (picStrokes p) 
      }
    append p1 p2 = Picture
      { picRegion = mkRegion (regionStart $ picRegion p1) (regionEnd $ picRegion p2)
      , picStrokes = picStrokes p1 ++ picStrokes p2
      }

$(declareClass "YiTextStorage" "NSTextStorage")
$(exportClass "YiTextStorage" "yts_" [
    InstanceVariable "buffer" [t| Maybe FBuffer |] [| Nothing |]
  , InstanceVariable "editor" [t| Editor |] [| emptyEditor |]
  , InstanceVariable "window" [t| Maybe Window |] [| Nothing |]
  , InstanceVariable "uiStyle" [t| Maybe UIStyle |] [| Nothing |]
  , InstanceVariable "dictionaryCache" [t| M.Map Attributes (NSDictionary ()) |] [| M.empty |]
  , InstanceVariable "pictureCache" [t| (Picture, NSRange) |] [| emptyPicture |]
  , InstanceVariable "stringCache" [t| Maybe (YiLBString ()) |] [| Nothing |]
  , InstanceMethod 'string -- '
  , InstanceMethod 'fixesAttributesLazily -- '
  , InstanceMethod 'attributeAtIndexEffectiveRange -- '
  , InstanceMethod 'attributesAtIndexEffectiveRange -- '
  , InstanceMethod 'attributesAtIndexLongestEffectiveRangeInRange
  , InstanceMethod 'replaceCharactersInRangeWithString -- '
  , InstanceMethod 'setAttributesRange     -- Disallow changing attributes
  , InstanceMethod 'addAttributesRange     -- optimized to avoid needless work
  , InstanceMethod 'addAttributeValueRange -- ...
  , InstanceMethod 'length -- '
  ])

yts_length :: YiTextStorage () -> IO CUInt
yts_length slf = do
  -- logPutStrLn "Calling yts_length "
  slf #. _stringCache >>= length . fromJust

yts_string :: YiTextStorage () -> IO (NSString ())
yts_string slf = castObject <$> fromJust <$> slf #. _stringCache

yts_fixesAttributesLazily :: YiTextStorage () -> IO Bool
yts_fixesAttributesLazily _ = return True

yts_attributesAtIndexEffectiveRange :: CUInt -> NSRangePointer -> YiTextStorage () -> IO (NSDictionary ())
yts_attributesAtIndexEffectiveRange i er slf = do
  (cache, _) <- slf #. _pictureCache
  if (fromIntegral i `inRegion` picRegion cache)
    then returnEffectiveRange cache i er (mkRegionRange $ picRegion cache) slf
    else yts_attributesAtIndexLongestEffectiveRangeInRange i er (NSRange i 1) slf

yts_attributesAtIndexLongestEffectiveRangeInRange :: CUInt -> NSRangePointer -> NSRange -> YiTextStorage () -> IO (NSDictionary ())
yts_attributesAtIndexLongestEffectiveRangeInRange i er rl slf = do
  (cache, prev_rl) <- slf #. _pictureCache
  -- Since we only cache the remaining part of the rl window, we must
  -- check to ensure that we do not re-read the window all the time...
  let use_rl = if prev_rl == rl then NSRange i (nsMaxRange rl) else rl
  -- logPutStrLn $ "yts_attributesAtIndexLongestEffectiveRangeInRange " ++ show i ++ " " ++ show rl
  ed <- slf #. _editor
  full <- extendPicture (mkRangeRegion use_rl) (flip (storagePicture ed) slf) cache
  -- TODO: Only merge identical strokes when "needed"?
  returnEffectiveRange full i er rl slf

returnEffectiveRange :: Picture -> CUInt -> NSRangePointer -> NSRange -> YiTextStorage () -> IO (NSDictionary ())
returnEffectiveRange full i er rl slf = do
  pic <- return $ dropStrokesWhile ((fromIntegral i >=) . fst) full
  -- logPutStrLn $ "returnEffectiveRange " ++ show pic
  slf # setIVar _pictureCache (pic, rl)
  if nullPicture pic
    then error "Empty picture?"
    else do
      let begin = fromIntegral $ regionStart $ picRegion pic
      let (next,s) = head $ picStrokes pic
      let end = min (fromIntegral next) (nsMaxRange rl)
      len <- yts_length slf
      safePoke er (NSRange begin ((min end len) - begin))
      -- Keep a cache of seen styles... usually, there should not be to many
      -- TODO: Have one centralized cache instead of one per text storage...
      dict <- slf # cachedDictionaryFor s
      -- TODO: Introduce some sort of cache for this...
      -- Create a new NSTextStorage to enforce Cocoa font-substitution
      str <- yts_string slf >>= substringWithRange (NSRange begin ((min end len) - begin))
      store <- _NSTextStorage # alloc >>= initWithStringAttributes str dict
      -- Extract the dictionary with adjusted fonts, and new (smaller) range
      dict2 <- store # attributesAtIndexEffectiveRange (i - begin) er
      when (er /= nullPtr) $ do
        -- If we got a range, we should offset it Offset the effective range accordingly
        NSRange b2 l2 <- peek er
        poke er (NSRange (begin + b2) l2)
      return dict2

cachedDictionaryFor :: Attributes -> YiTextStorage () -> IO (NSDictionary ())
cachedDictionaryFor s slf = do
  dicts <- slf #. _dictionaryCache
  case M.lookup s dicts of
    Just dict -> return dict
    _ -> do
      dict <- convertAttributes s
      slf # setIVar _dictionaryCache (M.insert s dict dicts)
      return dict

  

yts_attributeAtIndexEffectiveRange :: forall t. NSString t -> CUInt -> NSRangePointer -> YiTextStorage () -> IO (ID ())
yts_attributeAtIndexEffectiveRange attr i er slf = do
  attr' <- haskellString attr
  case attr' of
    "NSFont" -> do
      safePokeFullRange >> castObject <$> userFixedPitchFontOfSize 0 _NSFont
    "NSGlyphInfo" -> do
      safePokeFullRange >> return nil
    "NSAttachment" -> do
      safePokeFullRange >> return nil
    "NSCursor" -> do
      safePokeFullRange >> castObject <$> ibeamCursor _NSCursor
    "NSToolTip" -> do
      safePokeFullRange >> return nil
    "NSLanguage" -> do
      safePokeFullRange >> return nil
    "NSLink" -> do
      safePokeFullRange >> return nil
    "NSParagraphStyle" -> do
      -- TODO: Adjust line break property...
      safePokeFullRange >> castObject <$> defaultParagraphStyle _NSParagraphStyle
    "NSBackgroundColor" -> do
      -- safePokeFullRange >> castObject <$> blackColor _NSColor
      len <- yts_length slf
      ed <- slf #. _editor
      ~((s,a):_) <- onlyBg <$> picStrokes <$> slf # storagePicture ed (mkSizeRegion (fromIntegral i) strokeRangeExtent)
      safePoke er (NSRange i ((min len (fromIntegral s)) - i))
      castObject <$> getColor False (background a)
    _ -> do
      -- TODO: Optimize the other queries as well (if needed)
      -- logPutStrLn $ "Unoptimized yts_attributeAtIndexEffectiveRange " ++ attr' ++ " at " ++ show i
      super slf # attributeAtIndexEffectiveRange attr i er
  where
    safePokeFullRange = do
      Just b <- slf #. _buffer
      safePoke er (NSRange 0 (fromIntegral $ runBufferDummyWindow b sizeB))

-- These methods are used to modify the contents of the NSTextStorage.
-- We do not allow direct updates of the contents this way, though.
yts_replaceCharactersInRangeWithString :: forall t. NSRange -> NSString t -> YiTextStorage () -> IO ()
yts_replaceCharactersInRangeWithString _ _ _ = return ()
yts_setAttributesRange :: forall t. NSDictionary t -> NSRange -> YiTextStorage () -> IO ()
yts_setAttributesRange _ _ _ = return ()
yts_addAttributesRange :: NSDictionary t -> NSRange -> YiTextStorage () -> IO ()
yts_addAttributesRange _ _ _ = return ()
yts_addAttributeValueRange :: NSString t -> ID () -> NSRange -> YiTextStorage () -> IO ()
yts_addAttributeValueRange _ _ _ _ = return ()

-- | Remove element x_i if f(x_i,x_(i+1)) is true
filter2 :: (a -> a -> Bool) -> [a] -> [a]
filter2 _f [] = []
filter2 _f [x] = [x]
filter2 f (x1:x2:xs) =
  (if f x1 x2 then id else (x1:)) $ filter2 f (x2:xs)

-- | Keep only the background information
onlyBg :: [PicStroke] -> [PicStroke]
onlyBg = filter2 ((==) `on` (background . snd))

-- | Get a picture where each component (p,style) means apply the style
--   up until the given point p.
paintCocoaPicture :: UIStyle -> Point -> [PicStroke] -> [PicStroke]
paintCocoaPicture sty end = tail . stylesift (baseAttributes sty)
  where
    -- Turn a picture of use style from p into a picture of use style until p
    stylesift s [] = [(end,s)]
    stylesift s ((p,t):xs) = (p,s):(stylesift t xs)

-- | A version of poke that does nothing if p is null.
safePoke :: (Storable a) => Ptr a -> a -> IO ()
safePoke p x = when (p /= nullPtr) (poke p x)

-- | Execute strokeRangesB on the buffer, and update the buffer
--   so that we keep around cached syntax information...
--   We assume that the incoming region provide character-indices,
--   and we need to find out the corresponding byte-indices
storagePicture :: Editor -> Region -> YiTextStorage () -> IO Picture
storagePicture ed r slf = do
  Just sty <- slf #. _uiStyle
  Just buf <- slf #. _buffer
  Just win <- slf #. _window
  -- logPutStrLn $ "storagePicture " ++ show i
  return $ bufferPicture ed sty buf win r

byteToCharPicture :: Point -> [Point] -> [PicStroke] -> [PicStroke]
byteToCharPicture o [] xs = [(o,a) | (_,a) <- take 1 xs]
byteToCharPicture _ _ [] = []
byteToCharPicture o (p:ps) ((i,a):xs)
  | p < i = byteToCharPicture (succ o) ps ((i,a):xs)
  | otherwise = (o,a) : byteToCharPicture o (p:ps) xs 

bufferPicture :: Editor -> UIStyle -> FBuffer -> Window -> Region -> Picture
bufferPicture ed sty buf win r =
  let (r',is) = fst $ runBuffer win buf ((,) <$> byteRegionB r <*> indexStreamRegionB r)
  in Picture
  { picRegion = r
  , picStrokes =
      paintCocoaPicture sty (regionEnd r) $
        byteToCharPicture (regionStart r) is $
          (fst $ runBuffer win buf (attributesPictureB sty (regex ed) r' []))
  }

type TextStorage = YiTextStorage ()
initializeClass_TextStorage :: IO ()
initializeClass_TextStorage = do
  initializeClass_YiLBString
  initializeClass_YiTextStorage

applyUpdate :: YiTextStorage () -> FBuffer -> Update -> IO ()
applyUpdate buf b (Insert p _ s) =
  let p' = runBufferDummyWindow b (charIndexB p) in
  buf # editedRangeChangeInLength nsTextStorageEditedCharacters
          (NSRange (fromIntegral p') 0) (fromIntegral $ LB.length s)

applyUpdate buf b (Delete p _ s) =
  let (p', len) = (runBufferDummyWindow b (charIndexB p), LB.length s) in
  buf # editedRangeChangeInLength nsTextStorageEditedCharacters
          (NSRange (fromIntegral p') (fromIntegral len)) (fromIntegral (negate len))

newTextStorage :: UIStyle -> FBuffer -> Window -> IO TextStorage
newTextStorage sty b w = do
  buf <- new _YiTextStorage
  buf # setIVar _buffer (Just b)
  buf # setIVar _window (Just w)
  buf # setIVar _uiStyle (Just sty)
  s <- new _YiLBString
  s # setIVar _str (runBufferDummyWindow b (streamB Forward 0))
  buf # setIVar _stringCache (Just s)
  buf # setMonospaceFont
  return buf

setTextStorageBuffer :: Editor -> FBuffer -> TextStorage -> IO ()
setTextStorageBuffer ed buf storage = do
  storage # beginEditing
  Just s <- storage #. _stringCache
  s # setIVar _str (runBufferDummyWindow buf (streamB Forward 0))
  storage # setIVar _buffer (Just buf)
  storage # setIVar _editor ed
  when (not $ null $ pendingUpdates buf) $ do
    mapM_ (applyUpdate storage buf) [u | TextUpdate u <- pendingUpdates buf]
    storage # setIVar _pictureCache emptyPicture
  storage # endEditing

visibleRangeChanged :: NSRange -> TextStorage -> IO ()
visibleRangeChanged range storage = do
  storage # setIVar _pictureCache emptyPicture
  storage # editedRangeChangeInLength nsTextStorageEditedAttributes range 0