packages feed

manatee-editor-0.1.0: Manatee/Extension/Editor/SourceView.hs

-- Author:     Andy Stewart <lazycat.manatee@gmail.com>
-- Maintainer: Andy Stewart <lazycat.manatee@gmail.com>
-- 
-- Copyright (C) 2010 ~ 2011 Andy Stewart, all rights reserved.
-- 
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- any later version.
-- 
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
-- 
-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see <http://www.gnu.org/licenses/>.

{-# LANGUAGE ExistentialQuantification, RankNTypes, DeriveDataTypeable, ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}
module Manatee.Extension.Editor.SourceView where

import Control.Applicative
import Control.Concurrent.STM 
import Control.Monad
import Data.Map (Map)
import Data.Text.Lazy (Text)
import Data.Typeable
import Graphics.UI.Gtk hiding (Statusbar, statusbarNew, get)
import Graphics.UI.Gtk.SourceView.SourceLanguage
import Graphics.UI.Gtk.SourceView.SourceLanguageManager
import Manatee.Core.Config
import Manatee.Core.DBus
import Manatee.Core.PageFrame
import Manatee.Core.PageView
import Manatee.Core.Types
import Manatee.Extension.Editor.SourceBuffer
import Manatee.Toolkit.General.Basic
import Manatee.Toolkit.General.Maybe
import Manatee.Toolkit.General.STM
import Manatee.Toolkit.Gtk.Gtk
import Manatee.Toolkit.Gtk.Multiline
import Paths_manatee_editor
import System.FilePath
import Text.Printf

import qualified Data.Map as M
import qualified Graphics.UI.Gtk.SourceView.SourceBuffer as SB
import qualified Graphics.UI.Gtk.SourceView.SourceView as SV

data SourceView =
    SourceView {sourceViewPlugId          :: TVar PagePlugId
               ,sourceViewFrame           :: PageFrame
               ,sourceViewView            :: SV.SourceView
               ,sourceViewBuffer          :: SourceBuffer
               }
    deriving Typeable

instance PageBuffer SourceBuffer where
    pageBufferGetName           = readTVarIO . sourceBufferFilePath
    pageBufferSetName a         = writeTVarIO (sourceBufferFilePath a)
    pageBufferClient            = sourceBufferClient
    pageBufferCreateView a pId  = PageViewWrap <$> sourceViewNew a pId
    pageBufferMode              = sourceBufferMode
    pageBufferPackageName _     = fmap takeFileName getDataDir
    pageBufferWriteState        = sourceBufferWriteState
    pageBufferReadState         = sourceBufferReadState

instance PageView SourceView where
    pageViewBuffer               = PageBufferWrap . sourceViewBuffer
    pageViewPlugId               = sourceViewPlugId
    pageViewFrame                = sourceViewFrame
    pageViewCut                  = sourceViewCut
    pageViewCopy                 = sourceViewCopy
    pageViewPaste                = sourceViewPaste
    pageViewLocalKeymap _        = sourceViewLocalKeymap
    pageViewLocalCommandMap _    = sourceViewLocalCommandMap
    pageViewFocus                = widgetGrabFocus . sourceViewView
    pageViewPropagateWidget      = castToWidget . sourceViewView
    pageViewSaveState view       = sourceViewSaveState view Nothing
    pageViewRestoreState view    = sourceViewRestoreState view Nothing
    pageViewWriteState view path = sourceViewSaveState view (Just path)
    pageViewReadState view path  = sourceViewRestoreState view (Just path)
    pageViewScrollToTop          = sourceViewScrollToTop
    pageViewScrollToBottom       = sourceViewScrollToBottom
    pageViewScrollVerticalPage   = sourceViewScrollVerticalPage
    pageViewScrollVerticalStep   = sourceViewScrollVerticalStep

-- | Internal function for create string buffer.
sourceViewNew :: SourceBuffer -> PagePlugId -> IO SourceView
sourceViewNew sb plugId = do
  -- Create plug id.
  pId <- newTVarIO plugId

  -- Create UI frame.
  pFrame <- pageFrameNewWithModeName (pageModeName $ sourceBufferMode sb)

  sourceView <- SV.sourceViewNewWithBuffer (sourceBufferBuffer sb)
  pageFrameAddChild pFrame sourceView

  -- Set default font of source view.
  fontDescr <- fontDescriptionFromString "Monospace"
  widgetModifyFont sourceView (Just fontDescr)

  let sv = SourceView pId pFrame sourceView sb

  -- Load syntax highlight.
  sourceViewSyntaxHighlight sv

  -- Customize setup.
  SV.sourceViewSetHighlightCurrentLine sourceView True      -- highlight current line.
  SV.sourceViewSetInsertSpacesInsteadOfTabs sourceView True -- use space instead tabs
  SV.sourceViewSetShowLineNumbers sourceView True           -- show line number
  textViewSetCursorVisible sourceView True                  -- make cursor visible

  -- Update status after expose.
  sourceViewView sv `afterExposeRect` \ _ -> do
    sourceViewDisplayPositionStatus sv -- display position
    sourceViewDisplayPercentStatus sv  -- display percent

  -- Handle selection mark.
  sourceViewHandleSelectionMark sv

  return sv

-- | Get text buffer.
sourceViewGetTextBuffer :: SourceView -> IO TextBuffer 
sourceViewGetTextBuffer = textViewGetBuffer . sourceViewView

-- | Get source buffer.
sourceViewGetSourceBuffer :: SourceView -> IO SB.SourceBuffer
sourceViewGetSourceBuffer sb = 
    SB.castToSourceBuffer <$> sourceViewGetTextBuffer sb

-- | Get buffer content.
sourceViewGetText :: SourceView -> IO String
sourceViewGetText = textViewGetText . sourceViewView

-- | Get current line.
sourceViewGetLine :: SourceView -> IO Int
sourceViewGetLine = textViewGetLine . sourceViewView

-- | Get line count.
sourceViewGetLineCount :: SourceView -> IO Int
sourceViewGetLineCount = textViewGetLineCount . sourceViewView

-- | Get current column.
sourceViewGetColumn :: SourceView -> IO Int
sourceViewGetColumn = textViewGetColumn . sourceViewView

-- | Display position information.
sourceViewDisplayPositionStatus :: SourceView -> IO ()
sourceViewDisplayPositionStatus sb = 
  liftM2 (printf "Pos (%d, %d)") (sourceViewGetLine sb) (sourceViewGetColumn sb)
         >>= pageViewUpdateStatusbar sb "Pos"

-- | Display percent information.
sourceViewDisplayPercentStatus :: SourceView -> IO ()
sourceViewDisplayPercentStatus sv = do
  line <- sourceViewGetLine sv
  lineCount <- sourceViewGetLineCount sv
  pageViewUpdateStatusbar sv "Percent" ("(" ++ show (floor (i2d (line * 100) / i2d lineCount)) ++ "%)")
  
-- | Handle selection mark.
sourceViewHandleSelectionMark :: SourceView -> IO ()  
sourceViewHandleSelectionMark sv = do
  -- Get source view.
  let sourceView = sourceViewView sv 

  -- Cancel selection when button press.
  onButtonPress sourceView (\_ -> textViewCancelSelectionMark sourceView >> return False)

  -- Update selection iter after buffer changed.
  buffer <- textViewGetBuffer sourceView
  onBufferChanged buffer (textBufferUpdateSelectionIter buffer)

  -- Apply selection iter when focus in.
  onFocusIn sourceView (\_ -> sourceViewApplySelectionMark sv >> return False)

  return ()

-- | Get string buffer language.
sourceViewGetLanguage :: SourceView -> IO (Maybe SourceLanguage)
sourceViewGetLanguage buffer = 
    SB.sourceBufferGetLanguage
        =<< sourceViewGetSourceBuffer buffer

-- | Set string buffer language.
sourceViewSetLanguage :: SourceView -> SourceLanguage -> IO ()
sourceViewSetLanguage buffer language = 
    (<=<) (`SB.sourceBufferSetLanguage` Just language) sourceViewGetSourceBuffer buffer

-- | Syntax highlight with file name.
sourceViewSyntaxHighlight :: SourceView -> IO ()
sourceViewSyntaxHighlight view = do
  lm <- sourceLanguageManagerNew
  name <- sourceViewName view
  (_, lang) <- sourceLanguageForFilename lm (Just name)
  lang ?>= sourceViewSetLanguage view

-- | Save buffer.
sourceViewSave :: SourceView -> IO ()
sourceViewSave a = do
  filepath <- sourceViewName a
  string <- sourceViewGetText a
  writeFile filepath string
  name <- sourceViewName a
  pageViewShowOutputbar a ("Save " ++ name) Nothing

-- | Get buffer name.
sourceViewName :: SourceView -> IO String
sourceViewName = 
  pageBufferGetName . sourceViewBuffer

-- | Undo.
sourceViewUndo :: SourceView -> IO ()
sourceViewUndo a = do
  sb <- sourceViewGetSourceBuffer a
  ifM (SB.sourceBufferGetCanUndo sb)
          (do
            SB.sourceBufferUndo sb
            pageViewShowOutputbar a "Undo!" Nothing)
          (pageViewShowOutputbar a "No further undo information." Nothing)

-- | Redo.
sourceViewRedo :: SourceView -> IO ()
sourceViewRedo a = do
  sb <- sourceViewGetSourceBuffer a
  ifM (SB.sourceBufferGetCanRedo sb)
          (do
            SB.sourceBufferRedo sb
            pageViewShowOutputbar a "Redo!" Nothing)
          (pageViewShowOutputbar a "No further redo information." Nothing)

-- | String view wrap user action for undo/redo.
sourceViewWrapAction :: SourceView -> IO () -> IO ()  
sourceViewWrapAction  = textViewWrapAction . sourceViewView

-- | Newline.
sourceViewNewline :: SourceView -> IO ()  
sourceViewNewline = textViewNewLine . sourceViewView

-- | Open newline below.
sourceViewOpenNewlineBelow :: SourceView -> IO ()
sourceViewOpenNewlineBelow a = 
    textViewOpenNewlineBelow (sourceViewView a) (sourceViewScrolledWindow a)

-- | Open newline above.
sourceViewOpenNewlineAbove :: SourceView -> IO ()
sourceViewOpenNewlineAbove a = 
    textViewOpenNewlineAbove (sourceViewView a) (sourceViewScrolledWindow a)

-- | Select all.
sourceViewSelectAll :: SourceView -> IO ()
sourceViewSelectAll = textViewSelectAll . sourceViewView

-- | Delete.
sourceViewDelete :: SourceView -> IO ()
sourceViewDelete view = 
  textViewDelete (sourceViewView view) True True  >> return ()

-- | Cut.
sourceViewCut :: SourceView -> IO Bool
sourceViewCut view = do
  textViewCut $ sourceViewView view
  return True

-- | Copy.
sourceViewCopy :: SourceView -> IO Bool
sourceViewCopy view = do
  textViewCopy $ sourceViewView view
  return True

-- | Paste.
sourceViewPaste :: SourceView -> IO Bool
sourceViewPaste view = do
  textViewPaste $ sourceViewView view
  return True

-- | Forward line.
sourceViewForwardLine :: SourceView -> IO ()
sourceViewForwardLine a = do
    textViewForwardLine (sourceViewView a) (sourceViewScrolledWindow a)
    sourceViewApplySelectionMark a

-- | Backward line.
sourceViewBackwardLine :: SourceView -> IO ()
sourceViewBackwardLine a = do
    textViewBackwardLine (sourceViewView a) (sourceViewScrolledWindow a)
    sourceViewApplySelectionMark a

-- | Forward char.
sourceViewForwardChar :: SourceView -> IO ()
sourceViewForwardChar a = do
    textViewForwardChar (sourceViewView a) (sourceViewScrolledWindow a)
    sourceViewApplySelectionMark a

-- | Backward char.
sourceViewBackwardChar :: SourceView -> IO ()
sourceViewBackwardChar a = do
    textViewBackwardChar (sourceViewView a) (sourceViewScrolledWindow a)
    sourceViewApplySelectionMark a

-- | Forward word.
sourceViewForwardWord :: SourceView -> IO ()
sourceViewForwardWord a = do
    textViewForwardWord (sourceViewView a) (sourceViewScrolledWindow a)
    sourceViewApplySelectionMark a

-- | Backward word.
sourceViewBackwardWord :: SourceView -> IO ()
sourceViewBackwardWord a = do
    textViewBackwardWord (sourceViewView a) (sourceViewScrolledWindow a)
    sourceViewApplySelectionMark a

-- | Begin.
sourceViewScrollToTop :: SourceView -> IO ()
sourceViewScrollToTop a = do 
  textViewBegin (sourceViewView a) (sourceViewScrolledWindow a)
  sourceViewApplySelectionMark a

-- | End.
sourceViewScrollToBottom :: SourceView -> IO ()
sourceViewScrollToBottom a = do 
  textViewEnd (sourceViewView a) (sourceViewScrolledWindow a)
  sourceViewApplySelectionMark a

-- | Smart home.
sourceViewSmartHome :: SourceView -> IO ()
sourceViewSmartHome a = do
    textViewSmartHome $ sourceViewView a
    sourceViewApplySelectionMark a

-- | Smart end.
sourceViewSmartEnd :: SourceView -> IO ()
sourceViewSmartEnd a = do
    textViewSmartEnd $ sourceViewView a
    sourceViewApplySelectionMark a

-- | Delete forward char.
sourceViewDeleteForwardChar :: SourceView -> IO ()
sourceViewDeleteForwardChar view = 
  textViewDeleteForwardChar (sourceViewView view) False >> return ()

-- | Backward char.
sourceViewDeleteBackwardChar :: SourceView -> IO ()
sourceViewDeleteBackwardChar view = 
  textViewDeleteBackwardChar (sourceViewView view) False >> return ()

-- | Forward word.
sourceViewDeleteForwardWord :: SourceView -> IO ()
sourceViewDeleteForwardWord view = 
  textViewDeleteForwardWord (sourceViewView view) False >> return ()

-- | Backward word.
sourceViewDeleteBackwardWord :: SourceView -> IO ()
sourceViewDeleteBackwardWord view = 
  textViewDeleteBackwardWord (sourceViewView view) False >> return ()

-- | Delete to line end.
sourceViewDeleteToLineEnd :: SourceView -> IO ()
sourceViewDeleteToLineEnd view = 
  textViewDeleteToLineEnd (sourceViewView view) False >> return ()

-- | Delete to line start.
sourceViewDeleteToLineStart :: SourceView -> IO ()
sourceViewDeleteToLineStart view = 
  textViewDeleteToLineStart (sourceViewView view) False >> return ()

-- | Duplicate lines.
sourceViewDupLinesBelow, sourceViewDupLinesAbove :: SourceView -> IO ()
sourceViewDupLinesBelow = textViewDupLinesBelow . sourceViewView
sourceViewDupLinesAbove = textViewDupLinesAbove . sourceViewView

-- | Delete lines.
sourceViewDelLines :: SourceView -> IO ()
sourceViewDelLines view = 
  textViewDelLines (sourceViewView view) >> return ()

-- | Transposes lines.
sourceViewTraLinesBelow, sourceViewTraLinesAbove :: SourceView -> IO ()
sourceViewTraLinesBelow = textViewTraLinesBelow . sourceViewView
sourceViewTraLinesAbove = textViewTraLinesAbove . sourceViewView

-- | Reload file.
sourceViewReload :: SourceView -> IO ()
sourceViewReload sv = do
  -- Reload file.
  name <- sourceViewName sv
  textViewLoadFile (sourceViewView sv) name

  -- Move to view begin.
  sourceViewScrollToTop sv

-- | Set text.
sourceViewSetText :: SourceView -> String -> IO ()
sourceViewSetText = textViewSetText . sourceViewView

-- | Local keymap.
sourceViewLocalKeymap :: Map Text Text
sourceViewLocalKeymap = 
    M.fromList
         [("M-a",    "Select all")
         ,("M-s",    "Save")
         ,("M-d",    "Delete lines")
         ,("M-D",    "Delete")
         ,("M-/",    "Undo")
         ,("M-?",    "Redo")
         ,("M-r",    "Reload")
         ,("M-,",    "Backward delete char")
         ,("M-.",    "Forward delete char")
         ,("M-<",    "Backward delete word")
         ,("M->",    "Forward delete word")
         ,("C-M-,",  "Delete to line start")
         ,("C-M-.",  "Delete to line end")
         ,("M-j",    "Forward line")
         ,("M-k",    "Backward line")
         ,("M-l",    "Forward char")
         ,("M-h",    "Backward char")
         ,("M-m",    "New line")
         ,("Down",   "Forward line")
         ,("Up",     "Backward line")
         ,("Left",   "Forward char")
         ,("Right",  "Backward char")
         ,("Return", "New line")
         ,("M-L",    "Forward word")
         ,("M-H",    "Backward word")
         ,("M-P-h",  "Smart home")
         ,("M-P-l",  "Smart end")
         ,("M-N",    "Open new line below")
         ,("M-P",    "Open new line above")
         ,("M-w",    "Duplicate lines below")
         ,("M-W",    "Duplicate lines above")
         ,("M-e",    "Transposes line below")
         ,("M-E",    "Transposes line above")
         ,("C-c",    "Toggle selection mark")
         ,("C-C",    "Exchange selection mark")
         ,("C-o",    "Open file")
         ,("C-g",    "Goto line")
         ,("C-G",    "Goto column")
         ]

-- | String buffer keymap.
sourceViewLocalCommandMap :: Map Text (SourceView -> IO ())
sourceViewLocalCommandMap = 
    M.fromList 
         [("Select all",                sourceViewSelectAll)
         ,("Save",                      sourceViewSave)
         ,("Delete lines",              sourceViewDelLines)
         ,("Delete",                    sourceViewDelete)
         ,("Undo",                      sourceViewUndo)
         ,("Redo",                      sourceViewRedo)
         ,("Reload",                    sourceViewReload)
         ,("Backward delete char",      sourceViewDeleteBackwardChar)
         ,("Forward delete char",       sourceViewDeleteForwardChar)
         ,("Backward delete word",      sourceViewDeleteBackwardWord)
         ,("Forward delete word",       sourceViewDeleteForwardWord)
         ,("Delete to line start",      sourceViewDeleteToLineStart)
         ,("Delete to line end",        sourceViewDeleteToLineEnd)
         ,("Forward line",              sourceViewForwardLine)
         ,("Backward line",             sourceViewBackwardLine)
         ,("Forward char",              sourceViewForwardChar)
         ,("Backward char",             sourceViewBackwardChar)
         ,("New line",                  sourceViewNewline)
         ,("Forward word",              sourceViewForwardWord)
         ,("Backward word",             sourceViewBackwardWord)
         ,("Smart home",                sourceViewSmartHome)
         ,("Smart end",                 sourceViewSmartEnd)
         ,("Open new line below",       sourceViewOpenNewlineBelow)
         ,("Open new line above",       sourceViewOpenNewlineAbove)
         ,("Duplicate lines below",     sourceViewDupLinesBelow)
         ,("Duplicate lines above",     sourceViewDupLinesAbove)
         ,("Transposes line below",     sourceViewTraLinesBelow)
         ,("Transposes line above",     sourceViewTraLinesAbove)
         ,("Toggle selection mark",     sourceViewToggleSelectionMark)
         ,("Exchange selection mark",   sourceViewExchangeSelectionMark)
         ,("Open file",                 sourceViewOpenFile)
         ,("Goto line",                 sourceViewGotoLine)
         ,("Goto column",               sourceViewGotoColumn)
         ]

-- | Open file.
sourceViewOpenFile :: SourceView -> IO ()
sourceViewOpenFile view = 
  interactive view [(IFile, "Open file : ", "")] $ \ [path] ->                       
      mkDaemonSignal (pageViewClient view) NewTab (NewTabArgs "PageEditor" path [])

-- | Goto column.
sourceViewGotoColumn :: SourceView -> IO ()          
sourceViewGotoColumn view@SourceView {sourceViewView = sourceView} = 
  interactive view [(INum, "Column : ", "")] $ \ [column] -> do
      let number = read column :: Int
      textViewGotoColumn sourceView number
      pageViewShowOutputbar view (show [column]) Nothing
  
-- | Goto row.  
sourceViewGotoLine :: SourceView -> IO ()
sourceViewGotoLine view@SourceView {sourceViewView   = sourceView
                                   ,sourceViewBuffer = sourceBuffer} = do
  let buffer = sourceBufferBuffer sourceBuffer
  lines <- textBufferGetLineCount buffer
  interactive view [(INum, "Line (1 - " ++ show lines ++ ") : ", "")] $ \ [line] -> do
      let number = read line :: Int
      textViewGotoLine sourceView number

-- | Set selection mark.
sourceViewToggleSelectionMark :: SourceView -> IO ()
sourceViewToggleSelectionMark view = 
  ifM (textViewToggleSelectionMark $ sourceViewView view )
      (pageViewUpdateStatusbar view "Selection" "Selection (Active)")
      (pageViewUpdateStatusbar view "Selection" "Selection (Inactive)")

-- | Exchange selection mark.
sourceViewExchangeSelectionMark :: SourceView -> IO ()
sourceViewExchangeSelectionMark = textViewExchangeSelectionMark . sourceViewView 

-- | Show selection mark.
sourceViewApplySelectionMark :: SourceView -> IO ()
sourceViewApplySelectionMark = textViewApplySelectionMark . sourceViewView

-- | Scroll page vertically.
sourceViewScrollVerticalPage :: Bool -> SourceView -> IO ()
sourceViewScrollVerticalPage isDown a = do
  let sw = sourceViewScrolledWindow a
      tv = sourceViewView a
  pageInc <- (<=<) adjustmentGetPageIncrement scrolledWindowGetVAdjustment sw
  textViewScrollVertical tv sw (if isDown then pageInc else (- pageInc))
  sourceViewApplySelectionMark a

-- | Scroll step vertically.
sourceViewScrollVerticalStep :: Bool -> SourceView -> IO ()
sourceViewScrollVerticalStep isDown a = do
  let sw = sourceViewScrolledWindow a
      tv = sourceViewView a
  ti <- textViewGetTextIter tv
  (_, lineHeight) <- textViewGetLineYrange tv ti
  let stepInc = i2d lineHeight
  textViewScrollVertical tv sw (if isDown then stepInc else (- stepInc))
  sourceViewApplySelectionMark a

-- | Source view scrolled window.
sourceViewScrolledWindow :: SourceView -> ScrolledWindow    
sourceViewScrolledWindow =
  pageFrameScrolledWindow . sourceViewFrame

-- | Save state.
sourceViewSaveState :: SourceView -> Maybe FilePath -> IO ()
sourceViewSaveState view@(SourceView {sourceViewBuffer  = buffer}) 
                    statePath = do
  -- Get cursor.
  line <- textBufferGetLine (sourceBufferBuffer buffer)
  column <- textBufferGetLineOffset (sourceBufferBuffer buffer)

  -- Get scroll position.
  scrolledWindowPosition <- textViewGetCursorAlign (sourceViewView view)

  -- Save state.
  let state = SourceState (Just (line, column)) scrolledWindowPosition 
  case statePath of
    Nothing   -> writeTVarIO (sourceBufferState buffer) state
    Just path -> writeConfigPath path state

-- | Restore state.
sourceViewRestoreState :: SourceView -> Maybe FilePath -> IO ()
sourceViewRestoreState SourceView {sourceViewBuffer  = buffer
                                  ,sourceViewView    = textView} 
                       statePath = do
  bufferState <- readTVarIO (sourceBufferState buffer)
  (SourceState cursor scrolledWindowPosition) <- 
      case statePath of
        Just path -> readConfigPath path bufferState
        Nothing   -> return bufferState

  -- Restore cursor.
  cursor ?>= \ (line, column) -> do
    textViewGotoLine textView line
    textViewGotoColumn textView column

  -- Restore cursor alignment.
  -- TODO, we need find a way to restore TextView scroll position.
  -- textViewSetCursorAlign can't work if cursor not in visible area.
  textViewSetCursorAlign textView scrolledWindowPosition