{- | Editor integration. -} -- {-# LANGUAGE OverloadedStrings #-} module Hledger.UI.Editor ( -- TextPosition endPosition ,runEditor ,runIadd ) where import Control.Applicative ((<|>)) import Safe import System.Environment import System.Exit import System.FilePath import System.Process import Hledger -- | A position we can move to in a text editor: a line and optional column number. -- Line number 1 or 0 means the first line. A negative line number means the last line. type TextPosition = (Int, Maybe Int) -- | The text position meaning "last line, first column". endPosition :: Maybe TextPosition endPosition = Just (-1,Nothing) -- | Run the hledger-iadd executable on the given file, blocking until it exits, -- and return the exit code; or raise an error. -- hledger-iadd is an alternative to the built-in add command. runIadd :: FilePath -> IO ExitCode runIadd f = runCommand ("hledger-iadd -f " ++ f) >>= waitForProcess -- | Run the user's preferred text editor (or try a default editor), -- on the given file, blocking until it exits, and return the exit -- code; or raise an error. If a text position is provided, the editor -- will be focussed at that position in the file, if we know how. runEditor :: Maybe TextPosition -> FilePath -> IO ExitCode runEditor mpos f = editFileAtPositionCommand mpos f >>= runCommand >>= waitForProcess -- | Get a shell command line to open the user's preferred text editor -- (or a default editor) on the given file, and to focus it at the -- given text position if one is provided and if we know how. -- We know how to focus on position for: emacs, vi, nano. -- We know how to focus on last line for: vi. -- -- Some tests: With line and column numbers specified, -- @ -- if EDITOR is: the command should be: -- ------------- ----------------------------------- -- notepad notepad FILE -- vi vi +LINE FILE -- vi + FILE # negative LINE -- emacs emacs +LINE:COL FILE -- emacs FILE # negative LINE -- (unset) emacsclient -a '' -nw +LINE:COL FILE -- emacsclient -a '' -nw FILE # negative LINE -- @ -- -- How to open editors at the last line of a file: -- @ -- emacs: emacs FILE -f end-of-buffer -- emacsclient: can't -- vi: vi + FILE -- @ -- editFileAtPositionCommand :: Maybe TextPosition -> FilePath -> IO String editFileAtPositionCommand mpos f = do let f' = singleQuoteIfNeeded f editcmd <- getEditCommand let editor = lowercase $ takeFileName $ headDef "" $ words' editcmd let positionarg = case mpos of Just (l, mc) | editor `elem` [ "ex", "vi","vim","view","nvim","evim","eview", "gvim","gview","rvim","rview","rgvim","rgview" ] -> plusAndMaybeLine l mc Just (l, mc) | editor `elem` ["emacs", "emacsclient"] -> plusLineAndMaybeColonColumnOrEnd l mc Just (l, mc) | editor `elem` ["nano"] -> plusLineAndMaybeCommaColumn l mc _ -> "" where plusAndMaybeLine l _ = "+" ++ if l >= 0 then show l else "" plusLineAndMaybeCommaColumn l mc = "+" ++ show l ++ maybe "" ((","++).show) mc plusLineAndMaybeColonColumnOrEnd l mc | l >= 0 = "+" ++ show l ++ maybe "" ((":"++).show) mc | otherwise = "" -- otherwise = "-f end-of-buffer" -- XXX Problems with this: -- it must appear after the filename, whereas +LINE:COL must appear before -- it works only with emacs, not emacsclient return $ unwords [editcmd, positionarg, f'] -- | Get the user's preferred edit command. This is the value of the -- $HLEDGER_UI_EDITOR environment variable, or of $EDITOR, or a -- default ("emacsclient -a '' -nw", which starts/connects to an emacs -- daemon in terminal mode). getEditCommand :: IO String getEditCommand = do hledger_ui_editor_env <- lookupEnv "HLEDGER_UI_EDITOR" editor_env <- lookupEnv "EDITOR" let Just cmd = hledger_ui_editor_env <|> editor_env <|> Just "emacsclient -a '' -nw" return cmd