File: Editor.hs

package info (click to toggle)
haskell-hledger-ui 1.32.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, trixie
  • size: 420 kB
  • sloc: haskell: 2,443; makefile: 5
file content (140 lines) | stat: -rw-r--r-- 5,865 bytes parent folder | download
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