1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
|
{- | Editor integration. -}
module Hledger.UI.Editor (
-- TextPosition
endPosition
,runEditor
,runIadd
)
where
import Control.Applicative ((<|>))
import Data.List (intercalate)
import Data.Maybe (catMaybes)
import Data.Bifunctor (bimap)
import Safe
import System.Environment
import System.Exit
import System.FilePath
import System.Info (os)
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.
--
-- Just ('-' : _, _) is any text position with a negative line number.
-- A text position with a negative line number means the last line.
--
-- Some tests:
-- @
-- EDITOR program: Maybe TextPosition Command should be:
-- --------------- --------------------- ------------------------------------
-- emacs Just (line, Just col) emacs +LINE:COL FILE
-- Just (line, Nothing) emacs +LINE FILE
-- Just ('-' : _, _) emacs FILE -f end-of-buffer
-- Nothing emacs FILE
--
-- emacsclient Just (line, Just col) emacsclient +LINE:COL FILE
-- Just (line, Nothing) emacsclient +LINE FILE
-- Just ('-' : _, _) emacsclient FILE
-- Nothing emacsclient FILE
--
-- nano Just (line, Just col) nano +LINE:COL FILE
-- Just (line, Nothing) nano +LINE FILE
-- Just ('-' : _, _) nano FILE
-- Nothing nano FILE
--
-- vscode Just (line, Just col) vscode --goto FILE:LINE:COL
-- Just (line, Nothing) vscode --goto FILE:LINE
-- Just ('-' : _, _) vscode FILE
-- Nothing vscode FILE
--
-- kak Just (line, Just col) kak +LINE:COL FILE
-- Just (line, Nothing) kak +LINE FILE
-- Just ('-' : _, _) kak +: FILE
-- Nothing kak FILE
--
-- vi & variants Just (line, _) vi +LINE FILE
-- Just ('-' : _, _) vi + FILE
-- Nothing vi FILE
--
-- (other PROG) _ PROG FILE
--
-- (not set) Just (line, Just col) emacsclient -a '' -nw +LINE:COL FILE
-- Just (line, Nothing) emacsclient -a '' -nw +LINE FILE
-- Just ('-' : _, _) emacsclient -a '' -nw FILE
-- Nothing emacsclient -a '' -nw FILE
-- @
--
editFileAtPositionCommand :: Maybe TextPosition -> FilePath -> IO String
editFileAtPositionCommand mpos f = do
cmd <- getEditCommand
let editor = lowercase $ takeBaseName $ headDef "" $ words' cmd
f' = singleQuoteIfNeeded f
mpos' = Just . bimap show (fmap show) =<< mpos
join sep = intercalate sep . catMaybes
args = case editor of
"emacs" -> case mpos' of
Nothing -> [f']
Just ('-' : _, _) -> [f', "-f", "end-of-buffer"]
Just (l, mc) -> ['+' : join ":" [Just l, mc], f']
e | e `elem` ["emacsclient", "nano"] -> case mpos' of
Nothing -> [f']
Just ('-' : _, _) -> [f']
Just (l, mc) -> ['+' : join ":" [Just l, mc], f']
"vscode" -> case mpos' of
Nothing -> [f']
Just ('-' : _, _) -> [f']
Just (l, mc) -> ["--goto", join ":" [Just f', Just l, mc]]
"kak" -> case mpos' of
Nothing -> [f']
Just ('-' : _, _) -> ["+:", f']
Just (l, mc) -> ['+' : join ":" [Just l, mc], f']
e | e `elem` ["vi", "vim", "view", "nvim", "evim", "eview",
"gvim", "gview", "rvim", "rview",
"rgvim", "rgview", "ex"] -> case mpos' of
Nothing -> [f']
Just ('-' : _, _) -> ["+", f']
Just (l, _) -> ['+' : l, f']
_ -> [f']
return $ unwords $ cmd:args
-- | Get the user's preferred edit command. This is the value of the
-- $HLEDGER_UI_EDITOR environment variable, or of $EDITOR, or an OS-specific default.
--
-- For non-windows machines that would be "emacsclient -a '' -nw",
-- which starts/connects to an emacs daemon in terminal mode.
--
-- For windows the default is a plain "notepad.exe"
getEditCommand :: IO String
getEditCommand = do
hledger_ui_editor_env <- lookupEnv "HLEDGER_UI_EDITOR"
editor_env <- lookupEnv "EDITOR"
let defaultEditor = Just $ if os == "mingw32" then "notepad.exe" else "emacsclient -a '' -nw"
let Just cmd = hledger_ui_editor_env <|> editor_env <|> defaultEditor
return cmd
|