miv-0.4.9: src/VimScript.hs
module VimScript where
import Data.Char (isAlpha, isAlphaNum, isSpace, ord, toLower)
import Data.Default (def)
import Data.List (foldl', nub, sort)
import Data.List.Extra (groupSort)
import Data.Map.Strict qualified as M
import Data.Text (Text, singleton, unpack, unwords)
import Data.Text qualified as T
import Data.Text.Builder.Linear qualified as Builder
import Data.Text.Display (Display(..), display)
import Prelude hiding (unwords)
import Cmdline
import Command
import Mapping
import Mode
import Plugin
import Setting
newtype VimScript = VimScript (M.Map Place [Text])
deriving Eq
data Place = Plugin'
| Autoload Text
| Ftplugin Text
| Syntax Text
deriving (Eq, Ord)
instance Display Place where
displayBuilder = Builder.fromText . \case
Plugin' -> "plugin/miv.vim"
Autoload "" -> "autoload/miv.vim"
Autoload s -> "autoload/miv/" <> s <> ".vim"
Ftplugin s -> "ftplugin/" <> s <> ".vim"
Syntax s -> "syntax/" <> s <> ".vim"
vimScriptToList :: VimScript -> [(Place, [Text])]
vimScriptToList (VimScript x) = M.toList x
instance Semigroup VimScript where
VimScript x <> VimScript y
| M.null x = VimScript y
| M.null y = VimScript x
| otherwise = VimScript (M.unionWith concat' x y)
where
concat' a [] = a
concat' [] b = b
concat' a b = a <> [""] <> b
instance Monoid VimScript where
mempty = VimScript M.empty
gatherScript :: Setting -> VimScript
gatherScript setting = beforeScript setting
<> gatherBeforeAfterScript setting.plugins
<> gather' "dependon" (.dependon) (.loadbefore) setting.plugins
<> gather' "dependedby" (.dependedby) (.loadafter) setting.plugins
<> gather "mappings" (.mappings) setting.plugins
<> gather "mapmodes" (map display . (.mapmodes)) setting.plugins
<> gather "functions" (.functions) setting.plugins
<> pluginLoader
<> mappingLoader
<> commandLoader
<> foldl' (<>) mempty (map pluginConfig setting.plugins)
<> filetypeLoader setting
<> funcUndefinedLoader setting
<> cmdlineEnterLoader setting
<> insertEnterLoader setting
<> filetypeScript setting
<> syntaxScript setting
<> afterScript setting
gatherBeforeAfterScript :: [Plugin] -> VimScript
gatherBeforeAfterScript x = insertAuNameMap $ gatherScripts x (mempty, M.empty)
where
insertAuNameMap :: (VimScript, M.Map Text Text) -> VimScript
insertAuNameMap (vs, m) = VimScript (M.singleton (Autoload "") $
[ "let s:autoload = {" ]
<> [ " \\ " <> singleQuote k <> ": " <> singleQuote a <> "," | (k, a) <- M.toList m ]
<> [ " \\ }" ]) <> vs
gatherScripts :: [Plugin] -> (VimScript, M.Map Text Text) -> (VimScript, M.Map Text Text)
gatherScripts (p:ps) (vs, m)
| null p.before && null p.after = gatherScripts ps (vs, m)
| otherwise = gatherScripts ps (vs <> vs', M.insert name hchar m)
where
name = T.filter isAlphaNum (T.toLower (display p))
hchar | null (loadScript p) = maybe "_" singleton $ getHeadChar $ display p
| otherwise = "_"
funcname str = "miv#" <> hchar <> "#" <> str <> "_" <> name
vs' = VimScript $ M.singleton (Autoload hchar) $
wrapFunction (funcname "before") p.before <>
wrapFunction (funcname "after") p.after
gatherScripts [] (vs, m) = (vs, m)
gather :: Text -> (Plugin -> [Text]) -> [Plugin] -> VimScript
gather name f plugin
= VimScript (M.singleton (Autoload "") $
[ "let s:" <> name <> " = {" ]
<> [ " \\ " <> singleQuote (display p)
<> ": [ " <> T.intercalate ", " (map singleQuote (f p)) <> " ],"
| p <- plugin, p.enable /= "0", not (null (f p)) ]
<> [ " \\ }" ])
gather' :: Text -> (Plugin -> [Text]) -> (Plugin -> [Text]) -> [Plugin] -> VimScript
gather' name f g plugin
= VimScript (M.singleton (Autoload "") $
[ "let s:" <> name <> " = {" ]
<> [ " \\ " <> singleQuote p
<> ": [ " <> T.intercalate ", " (map singleQuote $ sort $ nub q) <> " ],"
| (p, q) <- groupSort $ [ (display p, q) | p <- plugin, p.enable /= "0", q <- f p ]
<> [ (q, display p) | p <- plugin, p.enable /= "0", q <- g p ] ]
<> [ " \\ }" ])
pluginConfig :: Plugin -> VimScript
pluginConfig plugin
= VimScript (M.singleton Plugin' $ wrapInfo $
wrapEnable plugin.enable $ mapleader <> gatherCommand plugin <> gatherMapping plugin <> plugin.script <> loadScript plugin)
where
wrapInfo [] = []
wrapInfo str = ("\" " <> plugin.name) : str
mapleader = let s = plugin.mapleader in ["let g:mapleader = " <> singleQuote s | not (T.null s)]
loadScript :: Plugin -> [Text]
loadScript plugin
| all null [ plugin.commands, plugin.mappings, plugin.functions, plugin.filetypes
, plugin.loadafter, plugin.loadbefore ] && null plugin.cmdlines && not plugin.insert
= ["call miv#load(" <> singleQuote (display plugin) <> ")"]
| otherwise = []
gatherCommand :: Plugin -> [Text]
gatherCommand plugin
| not (null plugin.commands)
= [display def
{ Command.name = c,
Command.repl = unwords [
"call miv#command(" <> singleQuote (display plugin) <> ",",
singleQuote c <> ",",
singleQuote "<bang>" <> ",",
"<q-args>,",
"expand('<line1>'),",
"expand('<line2>'))"
] } | c <- plugin.commands]
| otherwise = []
gatherMapping :: Plugin -> [Text]
gatherMapping plugin
| not (null plugin.mappings)
= [display def
{ Mapping.name = mapping,
Mapping.repl = escape mode <> ":<C-u>call miv#mapping("
<> singleQuote (display plugin) <> ", "
<> singleQuote mapping <> ", "
<> singleQuote (display mode) <> ")<CR>",
Mapping.mode = mode
} | mapping <- plugin.mappings, mode <- modes]
| otherwise = []
where modes = if null plugin.mapmodes then [NormalMode] else plugin.mapmodes
escape mode = if mode `elem` [InsertMode, OperatorPendingMode] then "<ESC>" else ""
beforeScript :: Setting -> VimScript
beforeScript setting = VimScript (M.singleton Plugin' setting.before)
afterScript :: Setting -> VimScript
afterScript setting = VimScript (M.singleton Plugin' setting.after)
filetypeLoader :: Setting -> VimScript
filetypeLoader setting =
mconcat $ map (uncurry f) $ groupSort [(ft, p) | p <- setting.plugins, ft <- p.filetypes]
where
f :: Text -> [Plugin] -> VimScript
f ft plugins = flip foldMap (getHeadChar ft) $
\c -> let funcname = "miv#" <> singleton c <> "#load_" <> T.filter isAlphaNum (T.toLower ft)
in VimScript (M.singleton Plugin'
[ "augroup miv-file-type-" <> ft
, " autocmd!"
, " autocmd FileType " <> ft <> " call " <> funcname <> "()"
, "augroup END" ]) <>
VimScript (M.singleton (Autoload (singleton c)) $
("function! " <> funcname <> "() abort")
: " setlocal filetype="
: loadPlugins plugins
<> [ " autocmd! miv-file-type-" <> ft
, " augroup! miv-file-type-" <> ft
, " setlocal filetype=" <> ft
, " silent! doautocmd FileType " <> ft
, "endfunction" ])
wrapEnable :: Text -> [Text] -> [Text]
wrapEnable _ [] = []
wrapEnable "" strs = strs
wrapEnable "0" _ = []
wrapEnable enable strs@(s:_) =
[indent <> "if " <> enable] <>
map (" "<>) strs <>
[indent <> "endif"]
where indent = T.takeWhile isSpace s
loadPlugins :: [Plugin] -> [Text]
loadPlugins plugins = concat
[wrapEnable enable
[" call miv#load(" <> singleQuote (display p) <> ")" | p <- plugins']
| (enable, plugins') <- groupSort [(p.enable, p) | p <- plugins]]
singleQuote :: Text -> Text
singleQuote str = "'" <> str <> "'"
filetypeScript :: Setting -> VimScript
filetypeScript setting = foldMap (\(ft, src) -> VimScript (M.singleton (Ftplugin ft) src)) $ M.toList setting.filetype
syntaxScript :: Setting -> VimScript
syntaxScript setting = foldMap (\(ft, src) -> VimScript (M.singleton (Syntax ft) src)) $ M.toList setting.syntax
wrapFunction :: Text -> [Text] -> [Text]
wrapFunction funcname script =
["function! " <> funcname <> "() abort"]
<> map (" "<>) script
<> ["endfunction"]
getHeadChar :: Text -> Maybe Char
getHeadChar xs
| T.null xs = Nothing
| otherwise = let x = T.head xs in if isAlpha x then Just (toLower x) else getHeadChar (T.tail xs)
mappingLoader :: VimScript
mappingLoader = VimScript (M.singleton (Autoload "")
[ "function! miv#mapping(name, mapping, mode) abort"
, " call miv#load(a:name)"
, " if a:mode ==# 'v' || a:mode ==# 'x'"
, " call feedkeys('gv', 'n')"
, " elseif a:mode ==# 'o'"
, " call feedkeys(\"\\<Esc>\", 'n')"
, " call feedkeys(v:operator, 'm')"
, " endif"
, " call feedkeys((v:count ? v:count : '') . substitute(a:mapping, '<Plug>', \"\\<Plug>\", 'g'), 'm')"
, " return ''"
, "endfunction" ])
commandLoader :: VimScript
commandLoader = VimScript (M.singleton (Autoload "")
[ "function! miv#command(name, command, bang, args, line1, line2) abort"
, " silent! execute 'delcommand' a:command"
, " call miv#load(a:name)"
, " let range = a:line1 != a:line2 ? a:line1.','.a:line2 : ''"
, " try"
, " exec range.a:command.a:bang a:args"
, " catch /^Vim\\%((\\a\\+)\\)\\=:E481:/"
, " exec a:command.a:bang a:args"
, " endtry"
, "endfunction" ])
funcUndefinedLoader :: Setting -> VimScript
funcUndefinedLoader setting = if null functions then mempty else
VimScript (M.singleton Plugin'
[ "\" FuncUndefined"
, "augroup miv-func-undefined"
, " autocmd!"
, " autocmd FuncUndefined * call miv#func_undefined(expand('<amatch>'))"
, "augroup END" ])
<> VimScript (M.singleton (Autoload "")
[ "function! miv#func_undefined(pattern) abort"
, " if a:pattern !~# " <> singleQuote (T.intercalate "\\|" functions)
, " return"
, " endif"
, " if empty(s:functions)"
, " autocmd! miv-func-undefined"
, " augroup! miv-func-undefined"
, " return"
, " endif"
, " for [name, fs] in items(s:functions)"
, " for f in fs"
, " if a:pattern =~# f"
, " call miv#load(name)"
, " return"
, " endif"
, " endfor"
, " endfor"
, "endfunction" ])
where functions = [f | p <- setting.plugins, f <- p.functions]
cmdlineEnterLoader :: Setting -> VimScript
cmdlineEnterLoader setting =
mconcat $ map (uncurry f) $ groupSort [(cmdline, p) | p <- setting.plugins, cmdline <- p.cmdlines]
where
f :: Cmdline -> [Plugin] -> VimScript
f cmdline plugins = VimScript (M.singleton Plugin'
[ "\" CmdlineEnter " <> display cmdline
, "if exists('#CmdlineEnter')"
, " augroup " <> group
, " autocmd!"
, " autocmd CmdlineEnter " <> cmdlinePattern cmdline <> " call miv#cmdline_enter_" <> c <> "()"
, " augroup END"
, "else"
, " call miv#cmdline_enter_" <> c <> "()"
, "endif" ])
<> VimScript (M.singleton (Autoload "") $
("function! miv#cmdline_enter_" <> c <> "() abort")
: loadPlugins plugins
<> [ " if exists('#CmdlineEnter')"
, " autocmd! " <> group
, " augroup! " <> group
, " endif"
, "endfunction" ])
where c = T.concat $ map (display . ord) (unpack (display cmdline))
group = "miv-cmdline-enter-" <> c
insertEnterLoader :: Setting -> VimScript
insertEnterLoader setting = if null plugins then mempty else
VimScript (M.singleton Plugin'
[ "\" InsertEnter"
, "augroup miv-insert-enter"
, " autocmd!"
, " autocmd InsertEnter * call miv#insert_enter()"
, "augroup END" ])
<> VimScript (M.singleton (Autoload "")
$ "function! miv#insert_enter() abort"
: loadPlugins plugins
<> [ " autocmd! miv-insert-enter"
, " augroup! miv-insert-enter"
, " silent! doautocmd InsertEnter"
, "endfunction" ])
where plugins = filter (.insert) setting.plugins
pluginLoader :: VimScript
pluginLoader = VimScript (M.singleton (Autoload "")
[ "let s:loaded = {}"
, "let s:path = expand('<sfile>:p:h:h')"
, "let s:mivpath = expand('<sfile>:p:h:h:h') . '/'"
, "let s:rtpidx = 0"
, "function! miv#load(name) abort"
, " if has_key(s:loaded, a:name)"
, " return"
, " endif"
, " let s:loaded[a:name] = 1"
, " if has_key(s:functions, a:name)"
, " call remove(s:functions, a:name)"
, " endif"
, " for n in get(s:dependon, a:name, [])"
, " call miv#load(n)"
, " endfor"
, " for mode in get(s:mapmodes, a:name, ['n'])"
, " for mapping in get(s:mappings, a:name, [])"
, " silent! execute mode . 'unmap' mapping"
, " endfor"
, " endfor"
, " let name = substitute(tolower(a:name), '[^a-z0-9]', '', 'g')"
, " let au = has_key(s:autoload, name)"
, " if au"
, " call miv#{s:autoload[name]}#before_{name}()"
, " endif"
, " let newrtp = s:mivpath . a:name"
, " if !isdirectory(newrtp)"
, " return"
, " endif"
, " let rtps = split(&rtp, ',')"
, " let s:rtpidx = max([index(rtps, s:path, s:rtpidx), 0])"
, " let &rtp = join(insert(rtps, newrtp, s:rtpidx), ',')"
, " if isdirectory(newrtp . '/after')"
, " exec 'set rtp+=' . newrtp . '/after'"
, " endif"
, " for dir in filter(['/plugin', '/after/plugin'], 'isdirectory(newrtp . v:val)')"
, " for file in split(glob(newrtp . dir . '/**/*.vim'), '\\n')"
, " silent! source `=file`"
, " endfor"
, " endfor"
, " if au"
, " call miv#{s:autoload[name]}#after_{name}()"
, " endif"
, " for n in get(s:dependedby, a:name, [])"
, " call miv#load(n)"
, " endfor"
, "endfunction" ])