├── LICENSE ├── README.md ├── example_dotfiles ├── base16-snazzy.lua ├── init.lua └── todo.lua └── lua └── nvim_utils.lua /LICENSE: -------------------------------------------------------------------------------- 1 | nvim_utils, a utility for neovim. 2 | Copyright © 2019 Ashkan Kiani 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim_utils.lua 2 | 3 | This is a copy of my progress migrating my init.vim to init.lua. 4 | 5 | The main utility here is `nvim_utils.lua`, and everything else is just an example of how 6 | I use it. 7 | 8 | This utility can be installed with any plugin manager, presumably, such as: 9 | 10 | ```vim 11 | Plug 'norcalli/nvim_utils' 12 | 13 | " Then after plug#end() 14 | 15 | lua require 'nvim_utils' 16 | ``` 17 | 18 | # Example 19 | 20 | ```lua 21 | local todo_mappings = require 'todo' 22 | 23 | function text_object_replace(is_visual_mode) 24 | local register = nvim.v.register 25 | local function replace() 26 | return nvim.fn.getreg(register, 1, 1) 27 | end 28 | if is_visual_mode then 29 | local visual_mode = nvim_visual_mode() 30 | nvim_buf_transform_region_lines(nil, '<', '>', visual_mode, replace) 31 | else 32 | nvim_text_operator_transform_selection(replace) 33 | end 34 | end 35 | 36 | local text_object_mappings = { 37 | ["n xr"] = { [[lua text_object_replace(false)]], noremap = true; }; 38 | ["x xr"] = { [[:lua text_object_replace(true)]], noremap = true; }; 39 | ["oil"] = { [[normal! $v^]], noremap = true; }; 40 | ["xil"] = { [[normal! $v^]], noremap = true; }; 41 | } 42 | 43 | local other_mappings = { 44 | ["nY"] = { [["+y]], noremap = true; }; 45 | ["xY"] = { [["+y]], noremap = true; }; 46 | -- Highlight current cword 47 | ["n[,"] = { function() 48 | -- \C forces matching exact case 49 | -- \M forces nomagic interpretation 50 | -- \< and \> denote whole word match 51 | nvim.fn.setreg("/", ([[\C\M\<%s\>]]):format(nvim.fn.expand("")), "c") 52 | nvim.o.hlsearch = true 53 | end }; 54 | ["i"] = { function() 55 | local pos = nvim.win_get_cursor(0) 56 | local line = nvim.buf_get_lines(0, pos[1] - 1, pos[1], false)[1] 57 | local _, start = line:find("^%s+") 58 | nvim.win_set_cursor(0, {pos[1], start}) 59 | end }; 60 | } 61 | 62 | local mappings = { 63 | text_object_mappings, 64 | other_mappings, 65 | } 66 | 67 | nvim_apply_mappings(vim.tbl_extend("error", unpack(mappings)), default_options) 68 | 69 | FILETYPE_HOOKS = { 70 | todo = function() 71 | nvim.command('setl foldlevel=2') 72 | nvim_apply_mappings(todo_mappings, { buffer = true }) 73 | end; 74 | } 75 | 76 | 77 | local autocmds = { 78 | todo = { 79 | {"BufEnter", "*.todo", "setl ft=todo"}; 80 | {"FileType", "todo", "lua FILETYPE_HOOKS.todo()"}; 81 | }; 82 | } 83 | 84 | nvim_create_augroups(autocmds) 85 | ``` 86 | 87 | 88 | # Things `nvim_utils` provides 89 | 90 | There are two types of things provided: 91 | 92 | - `nvim` is an object which contains shortcut/magic methods that are very useful for mappings 93 | - `nvim_*` functions which constitute building blocks for APIs like text operators or text manipulation or mappings 94 | 95 | ## Constants 96 | 97 | - `VISUAL_MODE.{line,char,block}` 98 | 99 | ## API Function and Command Shortcuts 100 | 101 | All of these methods cache the inital lookup in the metatable, but there is a small overhead regardless. 102 | 103 | - `nvim.$method(...)` redirects to `vim.api.nvim_$method(...)` 104 | - e.g. `nvim.command(...) == vim.api.nvim_command(...)`. 105 | - This is just for laziness. 106 | - `nvim.fn.$method(...)` redirects to `vim.api.nvim_call_function($method, {...})` 107 | - e.g. `nvim.fn.expand("%:h")` or `nvim.fn.has("terminal")` 108 | - `nvim.ex.$command(...)` is approximately `:$command flatten({...}).join(" ")` 109 | - e.g. `nvim.ex.edit("term://$SHELL")` or `nvim.ex.startinsert()` 110 | - Since `!` isn't a valid identifier character, you can use `_` at the end to indicate a `!` 111 | - e.g. `nvim.ex.nnoremap_("x", "echo hi")` 112 | 113 | ## Variable shortcuts 114 | 115 | - `nvim.g` can be used to get/set `g:` global variables. 116 | - e.g. `nvim.g.variable == g:variable` 117 | - `nvim.g.variable = 123` or `nvim.g.variable = nil` to delete the variable 118 | - `:h nvim_get_var` `:h nvim_set_var` `:h nvim_del_var` for more 119 | - `nvim.v` can be used to get/set `v:` variables. 120 | - e.g. `nvim.v.count1 == v:count1` 121 | - Useful `v:` variables, `v:register`, `v:count1`, etc.. 122 | - `nvim.v.variable = 123` to set the value (when not read-only). 123 | - `:h nvim_get_vvar` `:h nvim_set_vvar` for more 124 | - `nvim.b` can be used to get/set `b:` buffer variables for the current buffer. 125 | - e.g. `nvim.b.variable == b:variable` 126 | - `nvim.b.variable = 123` or `nvim.b.variable = nil` to delete the variable 127 | - `:h nvim_buf_get_var` `:h nvim_buf_set_var` `:h nvim_buf_del_var` for more 128 | - `nvim.env` can be used to get/set environment variables. 129 | - e.g. `nvim.env.PWD == $PWD` 130 | - `nvim.env.TEST = 123` to set the value. Equivalent to `let $TEST = 123`. 131 | - `:h setreg` `:h setreg` for more. These aren't API functions. 132 | - `nvim.o` can be used to get/set global options, as in `:h options` which are set through `set`. 133 | - e.g. `nvim.o.shiftwidth == &shiftwidth` 134 | - `nvim.o.shiftwidth = 8` is equivalent to `set shiftwidth=8` or `let &shiftwidth = 8` 135 | - `:h nvim_get_option` `:h nvim_set_option` for more. 136 | - `nvim.bo` can be used to get/set **buffer** options, as in `:h options` which are set through `setlocal`. 137 | - Only for the current buffer. 138 | - e.g. `nvim.bo.shiftwidth == &shiftwidth` 139 | - `nvim.bo.shiftwidth = 8` is equivalent to `setlocal shiftwidth=8` 140 | - `:h nvim_buf_get_option` `:h nvim_buf_set_option` for more. 141 | 142 | ## Extra API functions 143 | 144 | - `nvim_mark_or_index(buf, input)`: An enhanced version of nvim_buf_get_mark which also accepts: 145 | - A number as input: which is taken as a line number. 146 | - A pair, which is validated and passed through otherwise. 147 | - `nvim_buf_get_region_lines(buf, mark_a, mark_b, mode)`: Return the lines of the selection, respecting selection modes. 148 | - `buf` defaults to current buffer. `mark_a` defaults to `'<'`. `mark_b` defaults to `'>'`. `mode` defaults to `VISUAL_MODE.char` 149 | - `block` isn't implemented because I haven't gotten around to it yet. 150 | - Accepts all forms of input that `nvim_mark_or_index` accepts for `mark_a`/`mark_b`. 151 | - Returns a `List`. 152 | - `nvim_buf_set_region_lines(buf, mark_a, mark_b, mode, lines)`: Set the lines between the marks. 153 | - `buf` defaults to current buffer. `mark_a` defaults to `'<'`. `mark_b` defaults to `'>'`. 154 | - `lines` is a `List`. Can be greater or less than the number of lines in the region. It will add or delete lines. 155 | - Only `line` is currently implemented. This is because to support `char`, you must have knowledge of the existing lines, and 156 | I wasn't going to do potentially expensive operations with a hidden cost. 157 | - If you want to use `char` mode, you want to use `nvim_buf_transform_region_lines` instead. 158 | - Accepts all forms of input that `nvim_mark_or_index` accepts for `mark_a`/`mark_b`. 159 | - `nvim_buf_transform_region_lines(buf, mark_a, mark_b, mode, fn)`: Transform the lines by calling `fn(lines, visualmode) -> lines`. 160 | - `buf` defaults to current buffer. `mark_a` defaults to `'<'`. `mark_b` defaults to `'>'`. 161 | - `block` isn't implemented because I haven't gotten around to it yet. 162 | - `fn(lines, visualmode)` should return a list of lines to set in the region. 163 | - A result of `nil` will not modify the region. 164 | - A result of `{}` will be changed to `{""}` which empties the region. 165 | - Accepts all forms of input that `nvim_mark_or_index` accepts for `mark_a`/`mark_b`. 166 | - `nvim_set_selection_lines(lines)`: Literally just a shortcut to `nvim_buf_set_region_lines(0, '<', '>', VISUAL_MODE.line, lines)` 167 | - `nvim_selection(mode)` 168 | - `return table.concat(nvim_buf_get_region_lines(nil, '<', '>', mode or VISUAL_MODE.char), "\n")` 169 | - `nvim_text_operator(fn)`: Pass in a callback which will be called like `opfunc` is for `g@` text operators. 170 | - `fn(visualmode)` is the format. This doesn't receive any lines, so you can do anything here. 171 | - If you didn't know about text operators, I suggest `:h g@`. It sets the region described by motion following `g@` to `'[,']` 172 | - For example 173 | ```lua 174 | nvim_text_operator(function(visualmode) 175 | nvim_print(visualmode, nvim_mark_or_index('['), nvim_mark_or_index(']')) 176 | end) 177 | ``` 178 | - `nvim_text_operator_transform_selection(fn, force_visual_mode)`: Just like `nvim_text_operator`, but different. 179 | - `fn(lines, visualmode) -> lines` is the expected format for lines. 180 | - `force_visual_mode` can be used to override the visualmode from `nvim_text_operator` 181 | - Here's the definition 182 | ```lua 183 | function nvim_text_operator_transform_selection(fn, forced_visual_mode) 184 | return nvim_text_operator(function(visualmode) 185 | nvim_buf_transform_region_lines(nil, "[", "]", forced_visual_mode or visualmode, function(lines) 186 | return fn(lines, visualmode) 187 | end) 188 | end) 189 | end 190 | ``` 191 | - `nvim_visual_mode()`: calls `visualmode()` but returns one of `VISUAL_MODE` entries instead of `v, V, etc..` 192 | - `nvim_transform_cword(fn)`: self explanatory 193 | - `nvim_transform_cWORD(fn)`: self explanatory 194 | - `nvim_apply_mappings(mappings, default_options)` 195 | - `mappings` should be a dictionary. 196 | - The keys of mapping should start with the type of mapping, e.g. `n` for `normal`, `x` for `xmap`, `v` for `vmap`, `!` for `map!`, `o` for `omap` etc. The rest of the key is the mapping for that mode. 197 | - e.g. `n xr` is `nmap xr`. `o` is `omap ` etc. 198 | - The values should start with the value of the mapping, which is a string or a Lua function. 199 | - The rest of it are options like `silent`, `expr`, `nowait`, `unique`, or `buffer` 200 | - I implemented `buffer` support myself. I also implemented the Lua callback support. 201 | - You can peek at how this is done by `nvim_print(LUA_MAPPING, LUA_BUFFER_MAPPING)` 202 | - Other keys supported: 203 | - `dot_repeat: bool`. If you want to add support for `tpope/vim-repeat`, this will call `repeat#set` for lua function keybindings. 204 | - For a lot of examples, look at `example_dotfiles/init.lua:82`. 205 | - Example: 206 | ```lua 207 | local mappings = { 208 | ["n af"] = { "RustFmt", noremap = true; }; 209 | ["x af"] = { ":RustFmtRange", noremap = true; }; 210 | ["n AZ"] = { function() nvim_print("hi") end }; 211 | } 212 | nvim_apply_mappings(mappings, { buffer = true; silent = true; }) 213 | ``` 214 | - `nvim_create_augroups(definitions)` 215 | - `definitions` is a map of lists 216 | ```lua 217 | local autocmds = { 218 | todo = { 219 | {"BufEnter", "*.todo", "setl ft=todo"}; 220 | {"BufEnter", "*meus/todo/todo.txt", "setl ft=todo"}; 221 | {"BufReadCmd", "*meus/todo/todo.txt", [[silent call rclone#load("db:todo/todo.txt")]]}; 222 | {"BufWriteCmd", "*meus/todo/todo.txt", [[silent call rclone#save("db:todo/todo.txt")]]}; 223 | {"FileReadCmd", "*meus/todo/todo.txt", [[silent call rclone#load("db:todo/todo.txt")]]}; 224 | {"FileWriteCmd", "*meus/todo/todo.txt", [[silent call rclone#save("db:todo/todo.txt")]]}; 225 | }; 226 | vimrc = { 227 | {"BufWritePost init.vim nested source $MYVIMRC"}; 228 | {"FileType man setlocal nonumber norelativenumber"}; 229 | {"BufEnter term://* setlocal nonumber norelativenumber"}; 230 | }; 231 | } 232 | nvim_create_augroups(autocmds) 233 | ``` 234 | 235 | ## Additional functionality 236 | 237 | ### Utilities 238 | 239 | - `nvim_print(...)` is approximately `echo vim.inspect({...})` 240 | - it's also defined at `nvim.print` 241 | - This is useful for debugging. It can accept multiple arguments. 242 | - `nvim_echo(...)` is approximately `echo table.concat({...}, '\n')` 243 | - it's also defined at `nvim.echo` 244 | - It can accept multiple arguments and concatenates them with a space. 245 | 246 | ### Things Lua is missing 247 | 248 | - `string.startswith` 249 | - `string.endswith` 250 | -------------------------------------------------------------------------------- /example_dotfiles/base16-snazzy.lua: -------------------------------------------------------------------------------- 1 | require 'nvim_utils' 2 | 3 | local gui00 = "282a36" 4 | local gui01 = "34353e" 5 | local gui02 = "43454f" 6 | local gui03 = "78787e" 7 | local gui04 = "a5a5a9" 8 | local gui05 = "e2e4e5" 9 | local gui06 = "eff0eb" 10 | local gui07 = "f1f1f0" 11 | local gui08 = "ff5c57" 12 | local gui09 = "ff9f43" 13 | local gui0A = "f3f99d" 14 | local gui0B = "5af78e" 15 | local gui0C = "9aedfe" 16 | local gui0D = "57c7ff" 17 | local gui0E = "ff6ac1" 18 | local gui0F = "b2643c" 19 | 20 | -- " Terminal color definitions 21 | local cterm00 = "00" 22 | local cterm03 = "08" 23 | local cterm05 = "07" 24 | local cterm07 = "15" 25 | local cterm08 = "01" 26 | local cterm0A = "03" 27 | local cterm0B = "02" 28 | local cterm0C = "06" 29 | local cterm0D = "04" 30 | local cterm0E = "05" 31 | 32 | local cterm01, cterm02, cterm04, cterm06, cterm09, cterm0F 33 | 34 | if use_256_colorspace then 35 | cterm01 = "18" 36 | cterm02 = "19" 37 | cterm04 = "20" 38 | cterm06 = "21" 39 | cterm09 = "16" 40 | cterm0F = "17" 41 | else 42 | cterm01 = "10" 43 | cterm02 = "11" 44 | cterm04 = "12" 45 | cterm06 = "13" 46 | cterm09 = "09" 47 | cterm0F = "14" 48 | end 49 | 50 | -- " Neovim terminal colours 51 | if nvim.fn.has("nvim") then 52 | local terminal_color_0 = "#282a36" 53 | local terminal_color_1 = "#ff5c57" 54 | local terminal_color_2 = "#5af78e" 55 | local terminal_color_3 = "#f3f99d" 56 | local terminal_color_4 = "#57c7ff" 57 | local terminal_color_5 = "#ff6ac1" 58 | local terminal_color_6 = "#9aedfe" 59 | local terminal_color_7 = "#e2e4e5" 60 | local terminal_color_8 = "#78787e" 61 | local terminal_color_9 = "#ff5c57" 62 | local terminal_color_10 = "#5af78e" 63 | local terminal_color_11 = "#f3f99d" 64 | local terminal_color_12 = "#57c7ff" 65 | local terminal_color_13 = "#ff6ac1" 66 | local terminal_color_14 = "#9aedfe" 67 | local terminal_color_15 = "#f1f1f0" 68 | local terminal_color_background = terminal_color_0 69 | local terminal_color_foreground = terminal_color_5 70 | if nvim.o.background == "light" then 71 | local terminal_color_background = terminal_color_7 72 | local terminal_color_foreground = terminal_color_2 73 | end 74 | elseif nvim.fn.has("terminal") then 75 | local terminal_ansi_colors = { "#282a36", 76 | "#ff5c57", 77 | "#5af78e", 78 | "#f3f99d", 79 | "#57c7ff", 80 | "#ff6ac1", 81 | "#9aedfe", 82 | "#e2e4e5", 83 | "#78787e", 84 | "#ff5c57", 85 | "#5af78e", 86 | "#f3f99d", 87 | "#57c7ff", 88 | "#ff6ac1", 89 | "#9aedfe", 90 | "#f1f1f0", 91 | } 92 | end 93 | 94 | -- nvim.command "hi clear" 95 | -- nvim.command "syntax reset" 96 | 97 | local function highlight(group, guifg, guibg, ctermfg, ctermbg, attr, guisp) 98 | local parts = {group} 99 | if guifg then table.insert(parts, "guifg=#"..guifg) end 100 | if guibg then table.insert(parts, "guibg=#"..guibg) end 101 | if ctermfg then table.insert(parts, "ctermfg="..ctermfg) end 102 | if ctermbg then table.insert(parts, "ctermbg="..ctermbg) end 103 | if attr then 104 | table.insert(parts, "gui="..attr) 105 | table.insert(parts, "cterm="..attr) 106 | end 107 | if guisp then table.insert(parts, "guisp=#"..guisp) end 108 | -- nvim_print(parts) 109 | -- nvim.ex.highlight(parts) 110 | vim.api.nvim_command('highlight '..table.concat(parts, ' ')) 111 | end 112 | 113 | -- Vim editor colors 114 | highlight("Normal", gui05, gui00, cterm05, cterm00, nil, nil) 115 | highlight("Bold", nil, nil, nil, nil, "bold", nil) 116 | highlight("Debug", gui08, nil, cterm08, nil, nil, nil) 117 | highlight("Directory", gui0D, nil, cterm0D, nil, nil, nil) 118 | highlight("Error", gui00, gui08, cterm00, cterm08, nil, nil) 119 | highlight("ErrorMsg", gui08, gui00, cterm08, cterm00, nil, nil) 120 | highlight("Exception", gui08, nil, cterm08, nil, nil, nil) 121 | highlight("FoldColumn", gui0C, gui01, cterm0C, cterm01, nil, nil) 122 | highlight("Folded", gui03, gui01, cterm03, cterm01, nil, nil) 123 | highlight("IncSearch", gui01, gui09, cterm01, cterm09, "none", nil) 124 | highlight("Italic", nil, nil, nil, nil, "none", nil) 125 | highlight("Macro", gui08, nil, cterm08, nil, nil, nil) 126 | highlight("MatchParen", nil, gui03, nil, cterm03, nil, nil) 127 | highlight("ModeMsg", gui0B, nil, cterm0B, nil, nil, nil) 128 | highlight("MoreMsg", gui0B, nil, cterm0B, nil, nil, nil) 129 | highlight("Question", gui0D, nil, cterm0D, nil, nil, nil) 130 | highlight("Search", gui01, gui0A, cterm01, cterm0A, nil, nil) 131 | highlight("Substitute", gui01, gui0A, cterm01, cterm0A, "none", nil) 132 | highlight("SpecialKey", gui03, nil, cterm03, nil, nil, nil) 133 | highlight("TooLong", gui08, nil, cterm08, nil, nil, nil) 134 | highlight("Underlined", gui08, nil, cterm08, nil, nil, nil) 135 | highlight("Visual", nil, gui02, nil, cterm02, nil, nil) 136 | highlight("VisualNOS", gui08, nil, cterm08, nil, nil, nil) 137 | highlight("WarningMsg", gui08, nil, cterm08, nil, nil, nil) 138 | highlight("WildMenu", gui08, gui0A, cterm08, nil, nil, nil) 139 | highlight("Title", gui0D, nil, cterm0D, nil, "none", nil) 140 | highlight("Conceal", gui0D, gui00, cterm0D, cterm00, nil, nil) 141 | highlight("Cursor", gui00, gui05, cterm00, cterm05, nil, nil) 142 | highlight("NonText", gui03, nil, cterm03, nil, nil, nil) 143 | highlight("LineNr", gui03, gui01, cterm03, cterm01, nil, nil) 144 | highlight("SignColumn", gui03, gui01, cterm03, cterm01, nil, nil) 145 | highlight("StatusLine", gui04, gui02, cterm04, cterm02, "none", nil) 146 | highlight("StatusLineNC", gui03, gui01, cterm03, cterm01, "none", nil) 147 | highlight("VertSplit", gui02, gui02, cterm02, cterm02, "none", nil) 148 | highlight("ColorColumn", nil, gui01, nil, cterm01, "none", nil) 149 | highlight("CursorColumn", nil, gui01, nil, cterm01, "none", nil) 150 | highlight("CursorLine", nil, gui01, nil, cterm01, "none", nil) 151 | highlight("CursorLineNr", gui04, gui01, cterm04, cterm01, nil, nil) 152 | highlight("QuickFixLine", nil, gui01, nil, cterm01, "none", nil) 153 | highlight("PMenu", gui05, gui01, cterm05, cterm01, "none", nil) 154 | highlight("PMenuSel", gui01, gui05, cterm01, cterm05, nil, nil) 155 | highlight("TabLine", gui03, gui01, cterm03, cterm01, "none", nil) 156 | highlight("TabLineFill", gui03, gui01, cterm03, cterm01, "none", nil) 157 | highlight("TabLineSel", gui0B, gui01, cterm0B, cterm01, "none", nil) 158 | 159 | -- Standard syntax highlighting 160 | highlight("Boolean", gui09, nil, cterm09, nil, nil, nil) 161 | highlight("Character", gui08, nil, cterm08, nil, nil, nil) 162 | highlight("Comment", gui03, nil, cterm03, nil, nil, nil) 163 | highlight("Conditional", gui0E, nil, cterm0E, nil, nil, nil) 164 | highlight("Constant", gui09, nil, cterm09, nil, nil, nil) 165 | highlight("Define", gui0E, nil, cterm0E, nil, "none", nil) 166 | highlight("Delimiter", gui0F, nil, cterm0F, nil, nil, nil) 167 | highlight("Float", gui09, nil, cterm09, nil, nil, nil) 168 | highlight("Function", gui0D, nil, cterm0D, nil, nil, nil) 169 | highlight("Identifier", gui08, nil, cterm08, nil, "none", nil) 170 | highlight("Include", gui0D, nil, cterm0D, nil, nil, nil) 171 | highlight("Keyword", gui0E, nil, cterm0E, nil, nil, nil) 172 | highlight("Label", gui0A, nil, cterm0A, nil, nil, nil) 173 | highlight("Number", gui09, nil, cterm09, nil, nil, nil) 174 | highlight("Operator", gui05, nil, cterm05, nil, "none", nil) 175 | highlight("PreProc", gui0A, nil, cterm0A, nil, nil, nil) 176 | highlight("Repeat", gui0A, nil, cterm0A, nil, nil, nil) 177 | highlight("Special", gui0C, nil, cterm0C, nil, nil, nil) 178 | highlight("SpecialChar", gui0F, nil, cterm0F, nil, nil, nil) 179 | highlight("Statement", gui08, nil, cterm08, nil, nil, nil) 180 | highlight("StorageClass", gui0A, nil, cterm0A, nil, nil, nil) 181 | highlight("String", gui0B, nil, cterm0B, nil, nil, nil) 182 | highlight("Structure", gui0E, nil, cterm0E, nil, nil, nil) 183 | highlight("Tag", gui0A, nil, cterm0A, nil, nil, nil) 184 | highlight("Todo", gui0A, gui01, cterm0A, cterm01, nil, nil) 185 | highlight("Type", gui0A, nil, cterm0A, nil, "none", nil) 186 | highlight("Typedef", gui0A, nil, cterm0A, nil, nil, nil) 187 | 188 | -- nvim.command 'syntax on' 189 | 190 | -------------------------------------------------------------------------------- /example_dotfiles/init.lua: -------------------------------------------------------------------------------- 1 | require 'nvim_utils' 2 | -- require 'base16-snazzy' 3 | local todo_mappings = require 'todo' 4 | 5 | function text_object_replace(is_visual_mode) 6 | local register = nvim.v.register 7 | local function replace() 8 | return nvim.fn.getreg(register, 1, 1) 9 | end 10 | if is_visual_mode then 11 | local visual_mode = nvim_visual_mode() 12 | nvim_buf_transform_region_lines(nil, '<', '>', visual_mode, replace) 13 | else 14 | -- It's unfortunate that lines must be fetched here considering it's not used, 15 | -- but for a "char" visual mode, it's required regardless. For lines, it wouldn't be, 16 | -- but that optimization is not made yet. 17 | -- TODO investigate if nvim_text_operator_transform_selection can be optimized 18 | nvim_text_operator_transform_selection(replace) 19 | end 20 | end 21 | 22 | local function duplicate(lines, mode) 23 | if mode == VISUAL_MODE.line then 24 | return vim.tbl_flatten {lines, lines} 25 | elseif mode == VISUAL_MODE.char then 26 | if #lines == 0 then 27 | return lines 28 | end 29 | if #lines == 1 then 30 | return {lines[1]..lines[1]} 31 | end 32 | local first_line = table.remove(lines, 1) 33 | local last_line = table.remove(lines) 34 | return vim.tbl_flatten {first_line, lines, last_line..first_line, lines, last_line} 35 | end 36 | end 37 | 38 | function text_object_duplicate(is_visual_mode) 39 | if is_visual_mode then 40 | local visual_mode = nvim_visual_mode() 41 | nvim_buf_transform_region_lines(nil, '<', '>', visual_mode, duplicate) 42 | else 43 | nvim_text_operator_transform_selection(duplicate) 44 | end 45 | end 46 | 47 | function text_object_comment_and_duplicate(is_visual_mode) 48 | local visual_mode = VISUAL_MODE.line 49 | local commentstring = nvim.bo.commentstring 50 | local function comment_dupe(lines) 51 | local commented = {} 52 | for _, line in ipairs(lines) do 53 | table.insert(commented, commentstring:format(line)) 54 | end 55 | return vim.tbl_flatten { lines, commented } 56 | end 57 | if is_visual_mode then 58 | nvim_buf_transform_region_lines(nil, '<', '>', visual_mode, comment_dupe) 59 | else 60 | nvim_text_operator_transform_selection(comment_dupe, visual_mode) 61 | end 62 | end 63 | 64 | local function text_object_define(mapping, function_name) 65 | local options = { silent = true, noremap = true } 66 | nvim.set_keymap('n', mapping, ("lua %s(%s)"):format(function_name, false), options) 67 | nvim.set_keymap('x', mapping, (":lua %s(%s)"):format(function_name, true), options) 68 | -- TODO figure out why mappings for this seem to not be working. 69 | -- nvim.set_keymap('x', mapping, ("lua %s(%s)"):format(function_name, true), options) 70 | -- nvim.ex.nnoremap {"", mapping, (":lua %s(%s)"):format(function_name, false)} 71 | -- nvim.ex.xnoremap {"", mapping, (":lua %s(%s)"):format(function_name, true)} 72 | end 73 | 74 | text_object_define(" xr", "text_object_replace") 75 | text_object_define(" xd", "text_object_duplicate") 76 | text_object_define(" xy", "text_object_comment_and_duplicate") 77 | text_object_define("gy", "text_object_comment_and_duplicate") 78 | 79 | -- local default_options = { silent = true; unique = true; } 80 | local default_options = { silent = true; } 81 | 82 | local text_object_mappings = { 83 | -- ["n xd"] = { [[lua text_object_duplicate(false)]], noremap = true; }; 84 | -- ["n xr"] = { [[lua text_object_replace(false)]], noremap = true; }; 85 | -- ["n xy"] = { [[lua text_object_comment_and_duplicate(false)]], noremap = true; }; 86 | -- ["x xd"] = { [[:lua text_object_duplicate(true)]], noremap = true; }; 87 | -- ["x xr"] = { [[:lua text_object_replace(true)]], noremap = true; }; 88 | -- ["x xy"] = { [[:lua text_object_comment_and_duplicate(true)]], noremap = true; }; 89 | ["n xdd"] = { [[ xd_]], }; 90 | ["n xrr"] = { [[ xr_]], }; 91 | ["n xyy"] = { [[ xyl]], }; 92 | ["oil"] = { [[normal! $v^]], noremap = true; }; 93 | ["xil"] = { [[normal! $v^]], noremap = true; }; 94 | ["oal"] = { [[normal! V]], noremap = true; }; 95 | ["xal"] = { [[normal! V]], noremap = true; }; 96 | ["oae"] = { [[normal! ggVG]], noremap = true; }; 97 | ["xae"] = { [[normal! ggVG]], noremap = true; }; 98 | ["o\\"] = { [[$]], noremap = true; }; 99 | ["x\\"] = { [[$]], noremap = true; }; 100 | } 101 | 102 | local function map_cmd(...) 103 | return { ("%s"):format(table.concat(vim.tbl_flatten {...}, " ")), noremap = true; } 104 | end 105 | 106 | local function map_set(...) 107 | return { ("silent set %s"):format(table.concat(vim.tbl_flatten {...}, " ")), noremap = true; } 108 | end 109 | 110 | local function toggle_settings(...) 111 | local parts = {} 112 | for _, setting in ipairs(vim.tbl_flatten{...}) do 113 | table.insert(parts, ("%s! %s?"):format(setting, setting)) 114 | end 115 | return parts 116 | end 117 | 118 | local function map_toggle_settings(...) 119 | local parts = {} 120 | for _, setting in ipairs(vim.tbl_flatten{...}) do 121 | table.insert(parts, ("%s! %s?"):format(setting, setting)) 122 | end 123 | return map_set(parts) 124 | end 125 | 126 | -- The mapping helps a lot with deduplicating, but you could still bypass it with key combos 127 | -- which are valid both in lowercase and uppercase like and 128 | local other_mappings = { 129 | -- Highlight current cword 130 | ["n[,"] = { function() 131 | -- \C forces matching exact case 132 | -- \M forces nomagic interpretation 133 | -- \< and \> denote whole word match 134 | nvim.fn.setreg("/", ([[\C\M\<%s\>]]):format(nvim.fn.expand("")), "c") 135 | nvim.o.hlsearch = true 136 | end }; 137 | -- Highlight current selection 138 | ["x[,"] = { function() 139 | local selection = table.concat(nvim_buf_get_region_lines(0, '<', '>', VISUAL_MODE.char), '\n') 140 | nvim.fn.setreg("/", ([[\C\M%s]]):format(selection), "c") 141 | nvim.o.hlsearch = true 142 | end }; 143 | ["n\\ "] = { function() 144 | nvim.put({" "}, "c", false, false) 145 | -- local pos = vim.api.nvim_win_get_cursor(0) 146 | -- nvim_buf_transform_region_lines(nil, pos, pos, VISUAL_MODE.char, function(lines) return {" "..lines[1]} end) 147 | end }; 148 | ["n\\"] = { function() 149 | -- nvim.put({""}, "c", false, false) 150 | local pos = vim.api.nvim_win_get_cursor(0) 151 | nvim_buf_transform_region_lines(nil, pos, pos, VISUAL_MODE.char, function(lines) 152 | return {"", lines[1]} 153 | end) 154 | -- TODO to indent or not indent? That is the question 155 | -- nvim.ex.normal_("=j") 156 | end }; 157 | ["n jj"] = { "\\" }; 158 | ["i"] = { function() 159 | local pos = nvim.win_get_cursor(0) 160 | local line = nvim.buf_get_lines(0, pos[1] - 1, pos[1], false)[1] 161 | nvim.win_set_cursor(0, {pos[1], #line}) 162 | end }; 163 | ["i"] = { function() 164 | local pos = nvim.win_get_cursor(0) 165 | local line = nvim.buf_get_lines(0, pos[1] - 1, pos[1], false)[1] 166 | local _, start = line:find("^%s+") 167 | nvim.win_set_cursor(0, {pos[1], start}) 168 | end }; 169 | ["i"] = { function() 170 | local pos = nvim.win_get_cursor(0) 171 | nvim_buf_transform_region_lines(0, pos, pos, VISUAL_MODE.line, function(lines) 172 | return {lines[1]:sub(1, pos[2])} 173 | end) 174 | end }; 175 | -- This should work, but mappings seem to have difficulty with this for some reason. 176 | -- ["v>"] = { function() 177 | -- nvim_buf_transform_region_lines(0, '<', '>', 'line', function(lines) 178 | -- local prefix 179 | -- if not nvim.bo.expandtab then 180 | -- prefix = "\t" 181 | -- else 182 | -- prefix = string.rep(" ", nvim.fn.shiftwidth()) 183 | -- end 184 | -- for i, line in ipairs(lines) do 185 | -- lines[i] = prefix..line 186 | -- end 187 | -- return lines 188 | -- end) 189 | -- end }; 190 | -- I like neosnippet expansion on therefore remap to 191 | ["i"] = { "", noremap = true; }; 192 | ["i"] = { "(neosnippet_expand_or_jump)", noremap = false; }; 193 | ["s"] = { "(neosnippet_expand_or_jump)", noremap = false; }; 194 | ["x"] = { "(neosnippet_expand_target)", noremap = false; }; 195 | 196 | -- Indent shit 197 | ["x>"] = { "normal! >gv", noremap = true; }; 198 | ["x<"] = { "normal! ", noremap = true; }; 199 | 200 | -- Misc bindings 201 | ["nQ"] = { "bd", noremap = true; }; 202 | -- This goes back a space, I wonder if there's a programmatic way to exit insert mode 203 | ["i"] = { "", noremap = true; }; 204 | -- Pop into editing a command quickly 205 | ["n :"] = { ":cc", noremap = true; }; 206 | 207 | -- Diff bindings 208 | ["n do"] = { "diffoff!", noremap = true; }; 209 | ["n dt"] = { "diffthis", noremap = true; }; 210 | ["n du"] = { "diffupdate", noremap = true; }; 211 | ["n dg"] = { "diffget", noremap = true; }; 212 | ["n dp"] = { "diffput", noremap = true; }; 213 | ["x dp"] = { "diffput", noremap = true; }; 214 | 215 | -- TODO insert these into mappings only if the appropriate plugins exist. 216 | -- git/vim-fugitive aliases/mappings 217 | ["n gS"] = { [[FZFGFiles?]], noremap = true; }; 218 | ["n gT"] = { [[FZFBCommits]], noremap = true; }; 219 | ["n gb"] = { [[Gblame]], noremap = true; }; 220 | ["n gc"] = { [[Gcommit]], noremap = true; }; 221 | ["n gd"] = { [[Gdiff]], noremap = true; }; 222 | ["n ge"] = { [[Gedit]], noremap = true; }; 223 | ["n gl"] = { [[Gpull]], noremap = true; }; 224 | ["n gp"] = { [[Gpush]], noremap = true; }; 225 | ["n gq"] = { [[Gcommit -m "%"]], noremap = true; }; 226 | ["n gr"] = { [[Gread]], noremap = true; }; 227 | ["n gs"] = { [[Gstatus]], noremap = true; }; 228 | ["n gt"] = { [[0Glog]], noremap = true; }; 229 | ["n gw"] = { [[Gwrite]], noremap = true; }; 230 | ["x gt"] = { [[:Glog]], noremap = true; }; 231 | 232 | ["n ldo"] = { [[LinediffReset]], noremap = true; }; 233 | ["n ldt"] = { [[Linediff]], noremap = true; }; 234 | ["x ldo"] = { [[LinediffReset]], noremap = true; }; 235 | ["x ldt"] = { [[Linediff]], noremap = true; }; 236 | 237 | ["n sw"] = map_toggle_settings("wrap"); 238 | ["n sn"] = map_toggle_settings("number", "relativenumber"); 239 | ["n sb"] = map_toggle_settings("scb"); 240 | ["n sp"] = map_toggle_settings("paste"); 241 | ["n sh"] = map_toggle_settings("list"); 242 | ["n sc"] = map_toggle_settings("hlsearch"); 243 | ["n sP"] = map_cmd("silent setlocal", toggle_settings("spell")); 244 | 245 | -- Open terminal at $PWD 246 | ["n at"] = { function() 247 | -- TODO use terminal api directly? 248 | nvim.ex.edit("term://$SHELL") 249 | nvim.ex.startinsert() 250 | end }; 251 | -- Open terminal at current buffer's directory 252 | ["n aT"] = { function() 253 | nvim.ex.edit(("term://%s//$SHELL"):format(nvim.fn.expand("%:h"))) 254 | nvim.ex.startinsert() 255 | end }; 256 | ["n ae"] = { "Explore", noremap = true }; 257 | 258 | -- Insert blank lines after the current line. 259 | ["n] "] = { function() 260 | local repetition = nvim.v.count1 261 | local pos = nvim.win_get_cursor(0) 262 | nvim_buf_transform_region_lines(0, pos, pos, VISUAL_MODE.line, function(lines) 263 | for _ = 1, repetition do 264 | table.insert(lines, '') 265 | end 266 | return lines 267 | end) 268 | end }; 269 | -- Insert blank lines before the current line. 270 | ["n[ "] = { function() 271 | local repetition = nvim.v.count1 272 | local pos = nvim.win_get_cursor(0) 273 | nvim_buf_transform_region_lines(0, pos, pos, VISUAL_MODE.line, function(lines) 274 | local result = {} 275 | for _ = 1, repetition do 276 | table.insert(result, '') 277 | end 278 | return vim.tbl_flatten {result, lines} 279 | end) 280 | nvim.win_set_cursor(0, {pos[1]+repetition, pos[2]}) 281 | end }; 282 | 283 | -- Transpose line downwards 284 | ["n]p"] = { function() 285 | nvim.put(nvim.fn.getreg(nvim.v.register, 1, true), "l", true, false) 286 | end }; 287 | ["n[p"] = { function() 288 | nvim.put(nvim.fn.getreg(nvim.v.register, 1, true), "l", false, false) 289 | end }; 290 | -- Transpose line downwards 291 | ["n]e"] = { function() 292 | local repetition = nvim.v.count1 293 | local pos = nvim.win_get_cursor(0) 294 | nvim_buf_transform_region_lines(0, pos, pos[1] + repetition, VISUAL_MODE.line, function(lines) 295 | table.insert(lines, table.remove(lines, 1)) 296 | return lines 297 | end) 298 | -- TODO Follow the line or not? 299 | nvim.win_set_cursor(0, {pos[1] + repetition, pos[2]}) 300 | end }; 301 | -- Transpose line upwards 302 | ["n[e"] = { function() 303 | local repetition = nvim.v.count1 304 | local pos = nvim.win_get_cursor(0) 305 | nvim_buf_transform_region_lines(0, pos[1] - repetition, pos, VISUAL_MODE.line, function(lines) 306 | table.insert(lines, 1, table.remove(lines)) 307 | return lines 308 | end) 309 | nvim.win_set_cursor(0, {pos[1] - repetition, pos[2]}) 310 | end }; 311 | 312 | ["nZZ"] = { function() 313 | if #nvim.list_bufs() > 1 then 314 | if not nvim.bo.modifiable then 315 | nvim.command("bd") 316 | else 317 | nvim.command("w | bd") 318 | end 319 | else 320 | nvim.command("xit") 321 | end 322 | -- if #vim.api.nvim_list_bufs() > 1 then 323 | -- if not vim.api.nvim_buf_get_option(vim.api.nvim_get_current_buf(), "modifiable") then 324 | -- vim.api.nvim_command("bd") 325 | -- else 326 | -- vim.api.nvim_command("w | bd") 327 | -- end 328 | -- else 329 | -- vim.api.nvim_command("xit") 330 | -- end 331 | end }; 332 | 333 | ["n"] = { [[gcl]] }; 334 | ["x"] = { [[normal gcl]], noremap = true; }; 335 | 336 | ["n"] = map_cmd("silent bprev"); 337 | ["n"] = map_cmd("silent bnext"); 338 | ["n"] = map_cmd("silent bnext"); 339 | 340 | ["i"] = map_cmd("silent bprev"); 341 | ["i"] = map_cmd("silent bnext"); 342 | ["i"] = map_cmd("silent bnext"); 343 | 344 | -- TODO replicate BD in lua, which is to delete the current buffer without 345 | -- closing the window. 346 | ["n"] = map_cmd("silent BD"); 347 | ["n"] = map_cmd("silent BD!"); 348 | ["i"] = map_cmd("silent BD"); 349 | ["i"] = map_cmd("silent BD!"); 350 | 351 | -- TODO redo in lua 352 | ["n"] = { [[n:set buftype=nofile]], noremap = true; }; 353 | 354 | -- TODO for some reason this doesn't work like I expect. 355 | -- ["ch"] = { [[expand("%:h")."/"]], nowait = true; noremap = true; expr = true; }; 356 | -- ["cp"] = { [[expand("%:p")]], nowait = true; noremap = true; expr = true; }; 357 | -- ["ct"] = { [[expand("%:t")]], nowait = true; noremap = true; expr = true; }; 358 | -- ["c"] = { [[expand("%")]], nowait = true; noremap = true; expr = true; }; 359 | 360 | -- ["i"] = { "l", noremap = true; }; 361 | -- TODO Try to do it programmatically 362 | -- ["i"] = { function() 363 | -- local pos = nvim.win_get_cursor(0) 364 | -- nvim.feedkeys("", 'n', false) 365 | -- -- nvim.ex.stopinsert() 366 | -- nvim.win_set_cursor(0, pos) 367 | -- end }; 368 | 369 | ["nY"] = { [["+y]], noremap = true; }; 370 | ["xY"] = { [["+y]], noremap = true; }; 371 | 372 | -- replace 'f' with 1-char Sneak 373 | ["n"] = { [[Sneak_f]] }; 374 | ["o"] = { [[Sneak_f]] }; 375 | ["x"] = { [[Sneak_f]] }; 376 | ["n"] = { [[Sneak_F]] }; 377 | ["o"] = { [[Sneak_F]] }; 378 | ["x"] = { [[Sneak_F]] }; 379 | 380 | -- replace 't' with 1-char Sneak 381 | ["n"] = { [[Sneak_t]] }; 382 | ["o"] = { [[Sneak_t]] }; 383 | ["x"] = { [[Sneak_t]] }; 384 | ["n"] = { [[Sneak_T]] }; 385 | ["o"] = { [[Sneak_T]] }; 386 | ["x"] = { [[Sneak_T]] }; 387 | 388 | ["ngx"] = map_cmd [[call jobstart(["ldo", expand("")])]]; 389 | -- I wish I could use for this. 390 | ["xgx"] = { [[:lua spawn("ldo", { args = { nvim_selection() } })]], noremap = true; }; 391 | } 392 | 393 | local motion_mappings = { 394 | ["n"] = { [[j]], noremap = true; }; 395 | ["n"] = { [[k]], noremap = true; }; 396 | ["n"] = { [[h]], noremap = true; }; 397 | ["n"] = { [[l]], noremap = true; }; 398 | ["n"] = { [[s]], noremap = true; }; 399 | ["n"] = { [[v]], noremap = true; }; 400 | ["n"] = { [[o]], noremap = true; }; 401 | 402 | ["n"] = { [[=]], noremap = true; }; 403 | ["n"] = { [[+]], noremap = true; }; 404 | ["n"] = { [[-]], noremap = true; }; 405 | ["n"] = { [[<]], noremap = true; }; 406 | ["n>"] = { [[>]], noremap = true; }; 407 | ["n"] = { [[q]], noremap = true; }; 408 | ["n"] = { [[n]], noremap = true; }; 409 | 410 | ["n"] = { [[z]], noremap = true; }; 411 | 412 | ["n"] = { [[H]], noremap = true; }; 413 | ["n"] = { [[J]], noremap = true; }; 414 | ["n"] = { [[K]], noremap = true; }; 415 | ["n"] = { [[L]], noremap = true; }; 416 | 417 | ["n"] = { [[]], noremap = true; }; 418 | ["n"] = { [[]], noremap = true; }; 419 | 420 | -- -- TODO autocreate these from the existing n 421 | -- ["i"] = { [[z]], noremap = true; }; 422 | -- ["i"] = { [[H]], noremap = true; }; 423 | -- ["i"] = { [[J]], noremap = true; }; 424 | -- ["i"] = { [[K]], noremap = true; }; 425 | -- ["i"] = { [[L]], noremap = true; }; 426 | } 427 | 428 | local insert_motion_mappings = {} 429 | for k, v in pairs(motion_mappings) do 430 | insert_motion_mappings["i"..k:sub(2)] = map_cmd("normal! "..v[1]) 431 | end 432 | 433 | local terminal_motion_mappings = {} 434 | for k, v in pairs(motion_mappings) do 435 | terminal_motion_mappings["t"..k:sub(2)] = { [[]]..v[1], noremap = true; } 436 | -- terminal_motion_mappings["t"..k:sub(2)] = map_cmd("stopinsert | normal! "..v[1]) 437 | end 438 | 439 | local mappings = { 440 | text_object_mappings, 441 | other_mappings, 442 | motion_mappings, 443 | insert_motion_mappings, 444 | terminal_motion_mappings, 445 | } 446 | 447 | nvim_apply_mappings(vim.tbl_extend("error", unpack(mappings)), default_options) 448 | 449 | FILETYPE_HOOKS = { 450 | todo = function() 451 | todo_mappings["n AZ"] = { function() 452 | nvim_print("hi") 453 | end } 454 | nvim.command('setl foldlevel=2') 455 | -- TODO why doesn't this work? 456 | -- nvim.bo.foldlevel = 2 457 | nvim_apply_mappings(todo_mappings, { buffer = true }) 458 | end; 459 | sql = function() 460 | nvim.bo.commentstring = "-- %s" 461 | nvim.bo.formatprg = "pg_format -" 462 | end; 463 | rust = function() 464 | local mappings = { 465 | ["n af"] = { "RustFmt", noremap = true; }; 466 | ["x af"] = { ":RustFmtRange", noremap = true; }; 467 | } 468 | nvim_apply_mappings(mappings, { buffer = true; silent = true; }) 469 | end; 470 | i3config = function() 471 | local mappings = { 472 | ["n ab"] = { ".w !xargs swaymsg --", noremap = true; }; 473 | ["x ab"] = { ":w !xargs swaymsg --", noremap = true; }; 474 | } 475 | nvim_apply_mappings(mappings, { buffer = true; silent = true; }) 476 | end; 477 | jq = function() 478 | nvim.bo.commentstring = "# %s" 479 | end; 480 | terraform = function() 481 | nvim.bo.commentstring = "# %s" 482 | end; 483 | ["play2-conf"] = function() 484 | nvim.bo.commentstring = "# %s" 485 | end; 486 | lua = function() 487 | local mappings = { 488 | -- ["n all"] = { [[.!lualambda]], noremap = true; }; 489 | -- ["x all"] = { [[:!lualambda]], noremap = true; }; 490 | ["n aL"] = { [[.!lualambda]], noremap = true; }; 491 | ["x aL"] = { [[:!lualambda]], noremap = true; }; 492 | ["n ab"] = { [[.luado loadstring(line)()]], noremap = true; }; 493 | ["x ab"] = { [[lua loadstring(nvim_selection())()]], noremap = true; }; 494 | } 495 | nvim_apply_mappings(mappings, { buffer = true; silent = true; }) 496 | end; 497 | json = function() 498 | -- setl formatprg=json_reformat shiftwidth=4 tabstop=4 499 | nvim.bo.formatprg = "json-reformat" 500 | nvim.bo.shiftwidth = 4 501 | nvim.bo.tabstop = 4 502 | end; 503 | crontab = function() 504 | nvim.bo.backup = false 505 | nvim.bo.writebackup = false 506 | end; 507 | matlab = function() 508 | nvim.bo.commentstring = "% %s" 509 | nvim.command [[command! -buffer Start term /Applications/MATLAB_R2015b.app/bin/matlab -nodesktop]] 510 | nvim.command [[command! -buffer Run let @"=expand("%:r.h")."\n\n" | b matlab | norm pa]] 511 | local mappings = { 512 | ["nm"] = map_cmd("Run"); 513 | } 514 | nvim_apply_mappings(mappings, { buffer = true; silent = true; }) 515 | end; 516 | go = function() 517 | local mappings = { 518 | ["ni"] = { "(go-info)" }; 519 | } 520 | nvim_apply_mappings(mappings, { buffer = true; silent = true; }) 521 | end; 522 | python = function() 523 | nvim.bo.makeprg = "python3 -mdoctest %" 524 | end; 525 | scala = function() 526 | local mappings = { 527 | ["n af"] = map_cmd [[!scalafmt %]]; 528 | } 529 | nvim_apply_mappings(mappings, { buffer = true; silent = true; }) 530 | 531 | nvim.bo.errorformat = table.concat({ 532 | [[%E %#[error] %f:%l: %m]], 533 | [[%C %#[error] %p^]], 534 | [[%C %#[error] %m]], 535 | [[%-C%.%#]], 536 | [[%Z]], 537 | [[%W %#[warn] %f:%l: %m]], 538 | [[%C %#[warn] %p^]], 539 | [[%C %#[warn] %m]], 540 | [[%-C%.%#]], 541 | [[%Z]], 542 | [[%-G%.%#]], 543 | }, ',') 544 | end; 545 | } 546 | 547 | local global_settings = { 548 | autoread = true; 549 | background = "dark"; 550 | cmdheight = 3; 551 | colorcolumn = "120"; 552 | diffopt = "internal,filler,vertical,iwhite", 553 | hidden = true; 554 | ignorecase = true; 555 | inccommand = "nosplit"; 556 | incsearch = true; 557 | laststatus = 2; 558 | listchars = [[eol:$,tab:>-,trail:~,extends:>,precedes:<]]; 559 | modeline = true; 560 | -- TODO why did I add this in the first place? 561 | path = nvim.o.path..","..nvim.env.PWD, 562 | printoptions = "bottom:1in"; 563 | shada = [[!,'500,<50,s10,h]]; 564 | showcmd = true; 565 | showmode = false; 566 | smartcase = true; 567 | splitbelow = true; 568 | -- Don't change the position to the start of the line on bnext or whatnot 569 | startofline = false; 570 | swapfile = false; 571 | termguicolors = true; 572 | title = true; 573 | titlestring = "%{join(split(getcwd(), '/')[-2:], '/')}"; 574 | viminfo = [[!,'300,<50,s10,h]]; 575 | wildignorecase = true; 576 | wildmenu = true; 577 | -- " set wildmode=list:longest,full 578 | wildmode = "longest:full,full"; 579 | 580 | -- Former settings 581 | -- set expandtab shiftwidth=2 softtabstop=2 tabstop=2 582 | -- relativenumber = true; 583 | -- number = true; 584 | } 585 | 586 | for name, value in pairs(global_settings) do 587 | nvim.o[name] = value 588 | end 589 | 590 | local autocmds = { 591 | todo = { 592 | {"BufEnter", "*.todo", "setl ft=todo"}; 593 | {"BufEnter", "*meus/todo/todo.txt", "setl ft=todo"}; 594 | {"BufReadCmd", "*meus/todo/todo.txt", [[silent call rclone#load("db:todo/todo.txt")]]}; 595 | {"BufWriteCmd", "*meus/todo/todo.txt", [[silent call rclone#save("db:todo/todo.txt")]]}; 596 | {"FileReadCmd", "*meus/todo/todo.txt", [[silent call rclone#load("db:todo/todo.txt")]]}; 597 | {"FileWriteCmd", "*meus/todo/todo.txt", [[silent call rclone#save("db:todo/todo.txt")]]}; 598 | }; 599 | vimrc = { 600 | {"BufWritePost init.vim nested source $MYVIMRC"}; 601 | {"FileType man setlocal nonumber norelativenumber"}; 602 | {"BufEnter term://* setlocal nonumber norelativenumber"}; 603 | }; 604 | } 605 | 606 | local function escape_keymap(key) 607 | -- Prepend with a letter so it can be used as a dictionary key 608 | return 'k'..key:gsub('.', string.byte) 609 | end 610 | 611 | for filetype, _ in pairs(FILETYPE_HOOKS) do 612 | -- Escape the name to be compliant with augroup names. 613 | autocmds["LuaFileTypeHook_"..escape_keymap(filetype)] = { 614 | {"FileType", filetype, ("lua FILETYPE_HOOKS[%q]()"):format(filetype)}; 615 | }; 616 | end 617 | 618 | nvim_create_augroups(autocmds) 619 | 620 | --[[ 621 | Use this line to automatically convert `..` mappings to `map_cmd ...` 622 | I'm not sure which version I like better. 623 | 624 | '<,'>s/{.*\(.*\).*/map_cmd [[\1]]; 625 | --]] 626 | -------------------------------------------------------------------------------- /example_dotfiles/todo.lua: -------------------------------------------------------------------------------- 1 | -- loadfile('/home/ashkan/.config/nvim/lua/nvim_utils.lua')() 2 | require 'nvim_utils' 3 | 4 | --- 5 | -- CORE UTILITIES 6 | --- 7 | -- These are the minimum core functions for parsing and manipulating todo lines which can be used 8 | -- to create user specific mappings and actions later on. 9 | 10 | -- A list of entries which are not to be considered as tags, such as protocol prefixes 11 | -- e.g. http://... would be considered a tag. 12 | TODO_TAG_BLOCKLIST = { http = true, https = true, git = true, zephyr = true, ftp = true, sftp = true } 13 | 14 | function todo_extract_tags(body) 15 | local tags = {} 16 | local function inserter(k, v) 17 | if not TODO_TAG_BLOCKLIST[k] then 18 | tags[k] = v 19 | return "" 20 | end 21 | end 22 | body = body:gsub(" ([%w%-_:]-):(%S+)", inserter) 23 | body = body:gsub("^([%w%-_:]-):(%S+) ?", inserter) 24 | return tags, body 25 | end 26 | 27 | function todo_extract_projects(body) 28 | local projects = {} 29 | local function inserter(v) 30 | table.insert(projects, v) 31 | return "" 32 | end 33 | body = body:gsub(" (%+%S+)", inserter) 34 | body = body:gsub("^(%+%S+) ?", inserter) 35 | return projects, body 36 | end 37 | 38 | function todo_extract_contexts(body) 39 | local contexts = {} 40 | local function inserter(v) 41 | table.insert(contexts, v) 42 | return "" 43 | end 44 | body = body:gsub(" (@%S+)", inserter) 45 | body = body:gsub("^(@%S+) ?", inserter) 46 | return contexts, body 47 | end 48 | 49 | function todo_parse(line) 50 | local is_completed 51 | line = line:gsub("^x ", function(v) is_completed = true; return "" end) 52 | local priority 53 | line = line:gsub("^(%([A-Z]%)) ", function(v) priority = v; return "" end) 54 | local date1 55 | line = line:gsub("^(%d%d%d%d%-%d%d?%-%d%d?) ", function(v) date1 = v; return "" end) 56 | local date2 57 | line = line:gsub("^(%d%d%d%d%-%d%d?%-%d%d?) ", function(v) date2 = v; return "" end) 58 | local creation, completion 59 | if date1 then 60 | if date2 then 61 | -- TODO sort these to assert their identity 62 | creation, completion = date2, date1 63 | else 64 | creation = date1 65 | end 66 | end 67 | 68 | return { 69 | is_completed = is_completed; 70 | priority = priority; 71 | creation = creation; 72 | completion = completion; 73 | body = line; 74 | projects = todo_extract_projects(line); 75 | contexts = todo_extract_contexts(line); 76 | tags = todo_extract_tags(line); 77 | } 78 | end 79 | 80 | function todo_parse_if_not_parsed(input) 81 | if type(input) == 'string' then 82 | return todo_parse(input) 83 | end 84 | return input 85 | end 86 | 87 | function todo_set_completion_date(input) 88 | local parsed = todo_parse_if_not_parsed(input) 89 | local parts = {} 90 | if parsed.is_completed then 91 | if parsed.priority then table.insert(parts, parsed.priority) end 92 | if parsed.creation then table.insert(parts, parsed.creation) end 93 | else 94 | table.insert(parts, "x") 95 | if parsed.priority then table.insert(parts, parsed.priority) end 96 | if parsed.creation then 97 | table.insert(parts, os.date("%Y-%m-%d")) 98 | table.insert(parts, parsed.creation) 99 | end 100 | end 101 | table.insert(parts, parsed.body) 102 | return table.concat(parts, " ") 103 | end 104 | 105 | function todo_format_prefix(parsed) 106 | local parts = {} 107 | if parsed.is_completed then table.insert(parts, "x") end 108 | if parsed.priority then table.insert(parts, parsed.priority) end 109 | if parsed.completion then table.insert(parts, parsed.completion) end 110 | if parsed.creation then table.insert(parts, parsed.creation) end 111 | return table.concat(parts, " ") 112 | end 113 | 114 | function todo_set_end_date_for_tag(input, tag) 115 | local parsed = todo_parse_if_not_parsed(input) 116 | if tag:match("start$") then 117 | local endtag = tag:gsub("start$", "end") 118 | if not parsed.tags[endtag] then 119 | parsed.tags[endtag] = os.date("%Y-%m-%dT%T%Z") 120 | end 121 | end 122 | return todo_format(parsed) 123 | end 124 | 125 | function todo_format(input, extra) 126 | local parsed = todo_parse_if_not_parsed(input) 127 | local parts = {} 128 | -- prefix 129 | do 130 | local prefix = todo_format_prefix(parsed) 131 | if #prefix > 0 then 132 | table.insert(parts, prefix) 133 | end 134 | end 135 | 136 | local tags, projects, contexts 137 | 138 | -- body 139 | local body = parsed.body 140 | tags, body = todo_extract_tags(body) 141 | if extra and extra.projects then projects, body = todo_extract_projects(body) end 142 | if extra and extra.contexts then contexts, body = todo_extract_contexts(body) end 143 | body = vim.trim(body) 144 | if #body > 0 then 145 | table.insert(parts, body) 146 | end 147 | 148 | if projects then 149 | table.sort(projects) 150 | parts = vim.tbl_flatten {parts, projects} 151 | end 152 | 153 | if contexts then 154 | table.sort(contexts) 155 | parts = vim.tbl_flatten {parts, contexts} 156 | end 157 | 158 | -- tags 159 | do 160 | local keys = {} 161 | for k, _ in pairs(parsed.tags) do 162 | table.insert(keys, k) 163 | end 164 | table.sort(keys) 165 | for _, k in ipairs(keys) do 166 | local v = parsed.tags[k] 167 | table.insert(parts, table.concat({k, v}, ":")) 168 | end 169 | end 170 | return table.concat(parts, " ") 171 | end 172 | 173 | function compare_lists(a, b) 174 | for i = 1,math.min(#a, #b) do 175 | if a[i] < b[i] then 176 | return true 177 | elseif a[i] > b[i] then 178 | return false 179 | end 180 | end 181 | if #a < #b then 182 | return true 183 | end 184 | return false 185 | end 186 | 187 | function tbl_remove_dups(a, b) 188 | -- TODO @performance could be improved 189 | local duplicates = {} 190 | for _, v in ipairs(a) do 191 | duplicates[v] = (duplicates[v] or 0) + 1 192 | end 193 | for _, v in ipairs(b) do 194 | duplicates[v] = (duplicates[v] or 0) + 1 195 | end 196 | local left = {} 197 | for _, v in ipairs(a) do 198 | if not (duplicates[v] and duplicates[v] > 1) then 199 | table.insert(left, v) 200 | end 201 | end 202 | local right = {} 203 | for _, v in ipairs(b) do 204 | if not (duplicates[v] and duplicates[v] > 1) then 205 | table.insert(right, v) 206 | end 207 | end 208 | return left, right 209 | end 210 | 211 | function todo_sort_lines(lines) 212 | for i, line in ipairs(lines) do 213 | lines[i] = {line, todo_parse(line)} 214 | end 215 | table.sort(lines, function(a, b) 216 | a, b = a[2], b[2] 217 | -- TODO @performance could be improved 218 | local contexts_a, contexts_b = tbl_remove_dups(a.contexts or {}, b.contexts or {}) 219 | local projects_a, projects_b = tbl_remove_dups(a.projects or {}, b.projects or {}) 220 | return compare_lists( 221 | vim.tbl_flatten {a.is_completed and 2 or 1, (a.priority or "(Z)"), a.tags.due or "9999-99-99", contexts_a, projects_a, a.body}, 222 | vim.tbl_flatten {b.is_completed and 2 or 1, (b.priority or "(Z)"), b.tags.due or "9999-99-99", contexts_b, projects_b, b.body} 223 | ) 224 | -- return (a[2].contexts[1] or "") < (b[2].contexts[1] or "") 225 | end) 226 | for i, line in ipairs(lines) do 227 | lines[i] = line[1] 228 | end 229 | return lines 230 | end 231 | 232 | --- 233 | -- MAPPINGS 234 | --- 235 | 236 | function todo_open_context(line) 237 | local parsed = todo_parse_if_not_parsed(line) 238 | local context = parsed.contexts[1] 239 | if context then 240 | if context:match("%.todo$") then 241 | nvim.ex.edit("%:h/"..context:sub(2)) 242 | end 243 | end 244 | end 245 | 246 | function todo_action_open_project(line) 247 | local parsed = todo_parse_if_not_parsed(line) 248 | local project = parsed.projects[1] 249 | if project then 250 | if project:match("%.todo$") then 251 | nvim.ex.edit("%:h/"..project:sub(2)) 252 | end 253 | end 254 | end 255 | 256 | function todo_action_try_open_cword() 257 | -- if nvim.fn.expand(""):sub(1,1) == "@"then 258 | local cword = nvim.fn.expand("") 259 | if cword:sub(1,1) == "+" then 260 | nvim.ex.edit("%:h/"..nvim.fn.expand(cword:sub(2))) 261 | else 262 | nvim.ex.norm_("gf") 263 | end 264 | end 265 | 266 | function todo_reformat(lines, extra) 267 | lines = todo_sort_lines(lines) 268 | for i, line in ipairs(lines) do 269 | lines[i] = todo_format(line, extra) 270 | end 271 | return lines 272 | end 273 | 274 | function todo_filter(lines, filterfn) 275 | local result = {} 276 | for _, line in ipairs(lines) do 277 | local parsed = todo_parse(line) 278 | if filterfn(parsed) then 279 | table.insert(result, line) 280 | end 281 | end 282 | return result 283 | end 284 | 285 | -- function todo_create_filter_from_this(cword) 286 | local function todo_create_filter_from_this(cword) 287 | local ctype = cword:sub(1,1) 288 | if ctype == "@" then 289 | -- TODO trim? 290 | return function(p) return vim.tbl_contains(p.contexts, cword) end 291 | elseif ctype == "+" then 292 | return function(p) return vim.tbl_contains(p.projects, cword) end 293 | end 294 | return function(p) return true end 295 | end 296 | 297 | -- TODO rename other functions to match this pattern where functions which are expected 298 | -- to be interface with mappings directly are called *_action_* 299 | function todo_action_filter(mark_a, mark_b) 300 | assert(false, "unimplemented") 301 | end 302 | 303 | function todo_action_reformat(is_visual_mode) 304 | local function reformat(lines) 305 | return todo_reformat(lines, nvim.g.todo_format_settings or {}) 306 | end 307 | if is_visual_mode then 308 | nvim_buf_transform_region_lines(nil, '<', '>', VISUAL_MODE.line, reformat) 309 | else 310 | nvim_text_operator_transform_selection(reformat, VISUAL_MODE.line) 311 | end 312 | end 313 | 314 | function todo_action_filter_by_cword(is_visual_mode) 315 | local filterfn = todo_create_filter_from_this(nvim.fn.expand("")) 316 | local function filter(lines) 317 | return todo_filter(lines, filterfn) 318 | end 319 | if is_visual_mode then 320 | nvim_buf_transform_region_lines(nil, '<', '>', VISUAL_MODE.line, filter) 321 | else 322 | nvim_text_operator_transform_selection(filter, VISUAL_MODE.line) 323 | end 324 | end 325 | 326 | function todo_action_set_end_date_for_tag_by_cword(line) 327 | local tag = nvim.fn.expand(""):match("^[^:]+") 328 | return todo_set_end_date_for_tag(line, tag) 329 | end 330 | 331 | return { 332 | ["ngf"] = { "lua todo_action_try_open_cword()", noremap = true; }; 333 | 334 | ["n AO"] = { ".luado todo_action_open_project(line)", noremap = true; }; 335 | 336 | ["n af"] = { "lua todo_action_reformat(false)", noremap = true; }; 337 | ["x af"] = { ":lua todo_action_reformat(true)", noremap = true; }; 338 | 339 | -- " TODO change AF to mean filter, which will look at cWORD and create a new 340 | -- " buffer which has the lines with that context/project @feature 341 | -- " Make sure to make the buffer unmodified like zephyr does on creation 342 | ["n AF"] = { "lua todo_action_filter_by_cword(false)", noremap = true; }; 343 | ["x AF"] = { ":lua todo_action_filter_by_cword(true)", noremap = true; }; 344 | -- ["n AF"] = { [[%luado return todo_filter({line}, todo_create_filter_from_this(nvim.fn.expand("")))[1] or ""]], noremap = true; }; 345 | 346 | ["n AE"] = { ".luado todo_action_set_end_date_for_tag_by_cword", noremap = true; }; 347 | 348 | ["n AC"] = { ".luado return todo_set_completion_date(line)", noremap = true; }; 349 | ["x AC"] = { ":luado return todo_set_completion_date(line)", noremap = true; }; 350 | } 351 | -------------------------------------------------------------------------------- /lua/nvim_utils.lua: -------------------------------------------------------------------------------- 1 | --- NVIM SPECIFIC SHORTCUTS 2 | local vim = vim 3 | local api = vim.api 4 | 5 | VISUAL_MODE = { 6 | line = "line"; -- linewise 7 | block = "block"; -- characterwise 8 | char = "char"; -- blockwise-visual 9 | } 10 | 11 | -- TODO I didn't know that api.nvim_buf_* methods could take 0 to signify the 12 | -- current buffer, so refactor potentially everything to avoid the call to 13 | -- api.nvim_get_current_buf 14 | 15 | -- An enhanced version of nvim_buf_get_mark which also accepts: 16 | -- - A number as input: which is taken as a line number. 17 | -- - A pair, which is validated and passed through otherwise. 18 | function nvim_mark_or_index(buf, input) 19 | if type(input) == 'number' then 20 | -- TODO how to handle column? It would really depend on whether this was the opening mark or ending mark 21 | -- It also doesn't matter as long as the functions are respecting the mode for transformations 22 | assert(input ~= 0, "Line number must be >= 1 or <= -1 for last line(s)") 23 | return {input, 0} 24 | elseif type(input) == 'table' then 25 | -- TODO Further validation? 26 | assert(#input == 2) 27 | assert(input[1] >= 1) 28 | return input 29 | elseif type(input) == 'string' then 30 | return api.nvim_buf_get_mark(buf, input) 31 | -- local result = api.nvim_buf_get_mark(buf, input) 32 | -- if result[2] == 2147483647 then 33 | -- result[2] = -1 34 | -- end 35 | -- return result 36 | end 37 | error(("nvim_mark_or_index: Invalid input buf=%q, input=%q"):format(buf, input)) 38 | end 39 | 40 | -- TODO should I be wary of `&selection` in the nvim_buf_get functions? 41 | --[[ 42 | " https://stackoverflow.com/questions/1533565/how-to-get-visually-selected-text-in-vimscript 43 | function! s:get_visual_selection() 44 | " Why is this not a built-in Vim script function?! 45 | let [line_start, column_start] = getpos("'<")[1:2] 46 | let [line_end, column_end] = getpos("'>")[1:2] 47 | let lines = getline(line_start, line_end) 48 | if len(lines) == 0 49 | return '' 50 | endif 51 | let lines[-1] = lines[-1][: column_end - (&selection == 'inclusive' ? 1 : 2)] 52 | let lines[0] = lines[0][column_start - 1:] 53 | return join(lines, "\n") 54 | endfunction 55 | --]] 56 | 57 | --- Return the lines of the selection, respecting selection modes. 58 | -- RETURNS: table 59 | function nvim_buf_get_region_lines(buf, mark_a, mark_b, mode) 60 | mode = mode or VISUAL_MODE.char 61 | buf = buf or api.nvim_get_current_buf() 62 | -- TODO keep these? @refactor 63 | mark_a = mark_a or '<' 64 | mark_b = mark_b or '>' 65 | 66 | local start = nvim_mark_or_index(buf, mark_a) 67 | local finish = nvim_mark_or_index(buf, mark_b) 68 | local lines = api.nvim_buf_get_lines(buf, start[1] - 1, finish[1], false) 69 | 70 | if mode == VISUAL_MODE.line then 71 | return lines 72 | end 73 | 74 | if mode == VISUAL_MODE.char then 75 | -- Order is important. Truncate the end first, because these are not commutative 76 | if finish[2] ~= 2147483647 then 77 | lines[#lines] = lines[#lines]:sub(1, finish[2] + 1) 78 | end 79 | if start[2] ~= 0 then 80 | lines[1] = lines[1]:sub(start[2] + 1) 81 | end 82 | return lines 83 | end 84 | 85 | local firstcol = start[2] + 1 86 | local lastcol = finish[2] 87 | if lastcol == 2147483647 then 88 | lastcol = -1 89 | else 90 | lastcol = lastcol + 1 91 | end 92 | for i, line in ipairs(lines) do 93 | lines[i] = line:sub(firstcol, lastcol) 94 | end 95 | return lines 96 | end 97 | 98 | function nvim_buf_set_region_lines(buf, mark_a, mark_b, mode, lines) 99 | buf = buf or api.nvim_get_current_buf() 100 | -- TODO keep these? @refactor 101 | mark_a = mark_a or '<' 102 | mark_b = mark_b or '>' 103 | 104 | assert(mode == VISUAL_MODE.line, "Other modes aren't supported yet") 105 | 106 | local start = nvim_mark_or_index(buf, mark_a) 107 | local finish = nvim_mark_or_index(buf, mark_b) 108 | 109 | return api.nvim_buf_set_lines(buf, start[1] - 1, finish[1], false, lines) 110 | end 111 | 112 | -- This is actually more efficient if what you're doing is modifying a region 113 | -- because it can save api calls. 114 | -- It's also the only way to do transformations that are correct with `char` mode 115 | -- since it has to have access to the initial values of the region lines. 116 | function nvim_buf_transform_region_lines(buf, mark_a, mark_b, mode, fn) 117 | buf = buf or api.nvim_get_current_buf() 118 | -- TODO keep these? @refactor 119 | mark_a = mark_a or '<' 120 | mark_b = mark_b or '>' 121 | 122 | local start = nvim_mark_or_index(buf, mark_a) 123 | local finish = nvim_mark_or_index(buf, mark_b) 124 | 125 | assert(start and finish) 126 | 127 | -- TODO contemplate passing in a function instead of lines as is. 128 | -- local lines 129 | -- local function lazy_lines() 130 | -- if not lines then 131 | -- lines = api.nvim_buf_get_lines(buf, start[1] - 1, finish[1], false) 132 | -- end 133 | -- return lines 134 | -- end 135 | 136 | local lines = api.nvim_buf_get_lines(buf, start[1] - 1, finish[1], false) 137 | 138 | local result 139 | if mode == VISUAL_MODE.char then 140 | local prefix = "" 141 | local suffix = "" 142 | -- Order is important. Truncate the end first, because these are not commutative 143 | -- TODO file a bug report about this, it's probably supposed to be -1 144 | if finish[2] ~= 2147483647 then 145 | suffix = lines[#lines]:sub(finish[2]+2) 146 | lines[#lines] = lines[#lines]:sub(1, finish[2] + 1) 147 | end 148 | if start[2] ~= 0 then 149 | prefix = lines[1]:sub(1, start[2]) 150 | lines[1] = lines[1]:sub(start[2] + 1) 151 | end 152 | result = fn(lines, mode) 153 | 154 | -- If I take the result being nil as leaving it unmodified, then I can use it 155 | -- to skip the set part and reuse this just to get fed the input. 156 | if result == nil then 157 | return 158 | end 159 | 160 | -- Sane defaults, assume that they want to erase things if it is empty 161 | if #result == 0 then 162 | result = {""} 163 | end 164 | 165 | -- Order is important. Truncate the end first, because these are not commutative 166 | -- TODO file a bug report about this, it's probably supposed to be -1 167 | if finish[2] ~= 2147483647 then 168 | result[#result] = result[#result]..suffix 169 | end 170 | if start[2] ~= 0 then 171 | result[1] = prefix..result[1] 172 | end 173 | elseif mode == VISUAL_MODE.line then 174 | result = fn(lines, mode) 175 | -- If I take the result being nil as leaving it unmodified, then I can use it 176 | -- to skip the set part and reuse this just to get fed the input. 177 | if result == nil then 178 | return 179 | end 180 | elseif mode == VISUAL_MODE.block then 181 | local firstcol = start[2] + 1 182 | local lastcol = finish[2] 183 | if lastcol == 2147483647 then 184 | lastcol = -1 185 | else 186 | lastcol = lastcol + 1 187 | end 188 | local block = {} 189 | for _, line in ipairs(lines) do 190 | table.insert(block, line:sub(firstcol, lastcol)) 191 | end 192 | result = fn(block, mode) 193 | -- If I take the result being nil as leaving it unmodified, then I can use it 194 | -- to skip the set part and reuse this just to get fed the input. 195 | if result == nil then 196 | return 197 | end 198 | 199 | if #result == 0 then 200 | result = {''} 201 | end 202 | for i, line in ipairs(lines) do 203 | local result_index = (i-1) % #result + 1 204 | local replacement = result[result_index] 205 | lines[i] = table.concat {line:sub(1, firstcol-1), replacement, line:sub(lastcol+1)} 206 | end 207 | result = lines 208 | end 209 | 210 | return api.nvim_buf_set_lines(buf, start[1] - 1, finish[1], false, result) 211 | end 212 | 213 | -- Equivalent to `echo vim.inspect(...)` 214 | function nvim_print(...) 215 | if select("#", ...) == 1 then 216 | api.nvim_out_write(vim.inspect((...))) 217 | else 218 | api.nvim_out_write(vim.inspect {...}) 219 | end 220 | api.nvim_out_write("\n") 221 | end 222 | 223 | --- Equivalent to `echo` EX command 224 | function nvim_echo(...) 225 | for i = 1, select("#", ...) do 226 | local part = select(i, ...) 227 | api.nvim_out_write(tostring(part)) 228 | -- api.nvim_out_write("\n") 229 | api.nvim_out_write(" ") 230 | end 231 | api.nvim_out_write("\n") 232 | end 233 | 234 | -- `nvim.$method(...)` redirects to `vim.api.nvim_$method(...)` 235 | -- `nvim.fn.$method(...)` redirects to `vim.api.nvim_call_function($method, {...})` 236 | -- TODO `nvim.ex.$command(...)` is approximately `:$command {...}.join(" ")` 237 | -- `nvim.print(...)` is approximately `echo vim.inspect(...)` 238 | -- `nvim.echo(...)` is approximately `echo table.concat({...}, '\n')` 239 | -- Both methods cache the inital lookup in the metatable, but there is a small overhead regardless. 240 | nvim = setmetatable({ 241 | print = nvim_print; 242 | echo = nvim_echo; 243 | fn = setmetatable({}, { 244 | __index = function(self, k) 245 | local mt = getmetatable(self) 246 | local x = mt[k] 247 | if x ~= nil then 248 | return x 249 | end 250 | local f = function(...) return api.nvim_call_function(k, {...}) end 251 | mt[k] = f 252 | return f 253 | end 254 | }); 255 | buf = setmetatable({ 256 | -- current = setmetatable({}, { 257 | -- __index = function(self, k) 258 | -- local mt = getmetatable(self) 259 | -- local x = mt[k] 260 | -- if x ~= nil then 261 | -- return x 262 | -- end 263 | -- local command = k:gsub("_$", "!") 264 | -- local f = function(...) return vim.api.nvim_command(command.." "..table.concat({...}, " ")) end 265 | -- mt[k] = f 266 | -- return f 267 | -- end 268 | -- }); 269 | }, { 270 | __index = function(self, k) 271 | local mt = getmetatable(self) 272 | local x = mt[k] 273 | if x ~= nil then 274 | return x 275 | end 276 | local f = api['nvim_buf_'..k] 277 | mt[k] = f 278 | return f 279 | end 280 | }); 281 | ex = setmetatable({}, { 282 | __index = function(self, k) 283 | local mt = getmetatable(self) 284 | local x = mt[k] 285 | if x ~= nil then 286 | return x 287 | end 288 | local command = k:gsub("_$", "!") 289 | local f = function(...) 290 | return api.nvim_command(table.concat(vim.tbl_flatten {command, ...}, " ")) 291 | end 292 | mt[k] = f 293 | return f 294 | end 295 | }); 296 | g = setmetatable({}, { 297 | __index = function(_, k) 298 | return api.nvim_get_var(k) 299 | end; 300 | __newindex = function(_, k, v) 301 | if v == nil then 302 | return api.nvim_del_var(k) 303 | else 304 | return api.nvim_set_var(k, v) 305 | end 306 | end; 307 | }); 308 | v = setmetatable({}, { 309 | __index = function(_, k) 310 | return api.nvim_get_vvar(k) 311 | end; 312 | __newindex = function(_, k, v) 313 | return api.nvim_set_vvar(k, v) 314 | end 315 | }); 316 | b = setmetatable({}, { 317 | __index = function(_, k) 318 | return api.nvim_buf_get_var(0, k) 319 | end; 320 | __newindex = function(_, k, v) 321 | if v == nil then 322 | return api.nvim_buf_del_var(0, k) 323 | else 324 | return api.nvim_buf_set_var(0, k, v) 325 | end 326 | end 327 | }); 328 | o = setmetatable({}, { 329 | __index = function(_, k) 330 | return api.nvim_get_option(k) 331 | end; 332 | __newindex = function(_, k, v) 333 | return api.nvim_set_option(k, v) 334 | end 335 | }); 336 | bo = setmetatable({}, { 337 | __index = function(_, k) 338 | return api.nvim_buf_get_option(0, k) 339 | end; 340 | __newindex = function(_, k, v) 341 | return api.nvim_buf_set_option(0, k, v) 342 | end 343 | }); 344 | env = setmetatable({}, { 345 | __index = function(_, k) 346 | return api.nvim_call_function('getenv', {k}) 347 | end; 348 | __newindex = function(_, k, v) 349 | return api.nvim_call_function('setenv', {k, v}) 350 | end 351 | }); 352 | }, { 353 | __index = function(self, k) 354 | local mt = getmetatable(self) 355 | local x = mt[k] 356 | if x ~= nil then 357 | return x 358 | end 359 | local f = api['nvim_'..k] 360 | mt[k] = f 361 | return f 362 | end 363 | }) 364 | 365 | nvim.option = nvim.o 366 | 367 | --- 368 | -- Higher level text manipulation utilities 369 | --- 370 | 371 | function nvim_set_selection_lines(lines) 372 | return nvim_buf_set_region_lines(nil, '<', '>', VISUAL_MODE.line, lines) 373 | end 374 | 375 | -- Return the selection as a string 376 | -- RETURNS: string 377 | function nvim_selection(mode) 378 | return table.concat(nvim_buf_get_region_lines(nil, '<', '>', mode or VISUAL_MODE.char), "\n") 379 | end 380 | 381 | -- TODO Use iskeyword 382 | -- WORD_PATTERN = "[%w_]" 383 | 384 | -- -- TODO accept buf or win as arguments? 385 | -- function nvim_transform_cword(fn) 386 | -- -- lua nvim_print(nvim.win_get_cursor(nvim.get_current_win())) 387 | -- local win = api.nvim_get_current_win() 388 | -- local row, col = unpack(api.nvim_win_get_cursor(win)) 389 | -- local buf = api.nvim_get_current_buf() 390 | -- -- local row, col = unpack(api.nvim_buf_get_mark(buf, '.')) 391 | -- local line = nvim_buf_get_region_lines(buf, row, row, VISUAL_MODE.line)[1] 392 | -- local start_idx, end_idx 393 | -- _, end_idx = line:find("^[%w_]+", col+1) 394 | -- end_idx = end_idx or (col + 1) 395 | -- if line:sub(col+1, col+1):match("[%w_]") then 396 | -- _, start_idx = line:sub(1, col+1):reverse():find("^[%w_]+") 397 | -- start_idx = col + 1 - (start_idx - 1) 398 | -- else 399 | -- start_idx = col + 1 400 | -- end 401 | -- local fragment = fn(line:sub(start_idx, end_idx)) 402 | -- local new_line = line:sub(1, start_idx-1)..fragment..line:sub(end_idx+1) 403 | -- nvim_buf_set_region_lines(buf, row, row, VISUAL_MODE.line, {new_line}) 404 | -- end 405 | 406 | -- Necessary glue for nvim_text_operator 407 | -- Calls the lua function whose name is g:lua_fn_name and forwards its arguments 408 | vim.api.nvim_command [[ 409 | function! LuaExprCallback(...) abort 410 | return luaeval(g:lua_expr, a:000) 411 | endfunction 412 | ]] 413 | 414 | function nvim_text_operator(fn) 415 | LUA_FUNCTION = fn 416 | nvim.g.lua_expr = 'LUA_FUNCTION(_A[1])' 417 | api.nvim_set_option('opfunc', 'LuaExprCallback') 418 | -- api.nvim_set_option('opfunc', 'v:lua.LUA_FUNCTION') 419 | api.nvim_feedkeys('g@', 'ni', false) 420 | end 421 | 422 | function nvim_text_operator_transform_selection(fn, forced_visual_mode) 423 | return nvim_text_operator(function(visualmode) 424 | nvim_buf_transform_region_lines(nil, "[", "]", forced_visual_mode or visualmode, function(lines) 425 | return fn(lines, visualmode) 426 | end) 427 | end) 428 | end 429 | 430 | function nvim_visual_mode() 431 | local visualmode = nvim.fn.visualmode() 432 | if visualmode == 'v' then 433 | return VISUAL_MODE.char 434 | elseif visualmode == 'V' then 435 | return VISUAL_MODE.line 436 | else 437 | return VISUAL_MODE.block 438 | end 439 | end 440 | 441 | function nvim_transform_cword(fn) 442 | nvim_text_operator_transform_selection(function(lines) 443 | return {fn(lines[1])} 444 | end) 445 | api.nvim_feedkeys('iw', 'ni', false) 446 | end 447 | 448 | function nvim_transform_cWORD(fn) 449 | nvim_text_operator_transform_selection(function(lines) 450 | return {fn(lines[1])} 451 | end) 452 | api.nvim_feedkeys('iW', 'ni', false) 453 | end 454 | 455 | function nvim_source_current_buffer() 456 | loadstring(table.concat(nvim_buf_get_region_lines(nil, 1, -1, VISUAL_MODE.line), '\n'))() 457 | end 458 | 459 | LUA_MAPPING = {} 460 | LUA_BUFFER_MAPPING = {} 461 | 462 | local function escape_keymap(key) 463 | -- Prepend with a letter so it can be used as a dictionary key 464 | return 'k'..key:gsub('.', string.byte) 465 | end 466 | 467 | local valid_modes = { 468 | n = 'n'; v = 'v'; x = 'x'; i = 'i'; 469 | o = 'o'; t = 't'; c = 'c'; s = 's'; 470 | -- :map! and :map 471 | ['!'] = '!'; [' '] = ''; 472 | } 473 | 474 | -- TODO(ashkan) @feature Disable noremap if the rhs starts with 475 | function nvim_apply_mappings(mappings, default_options) 476 | -- May or may not be used. 477 | local current_bufnr = api.nvim_get_current_buf() 478 | for key, options in pairs(mappings) do 479 | options = vim.tbl_extend("keep", options, default_options or {}) 480 | local bufnr = current_bufnr 481 | -- TODO allow passing bufnr through options.buffer? 482 | -- protect against specifying 0, since it denotes current buffer in api by convention 483 | if type(options.buffer) == 'number' and options.buffer ~= 0 then 484 | bufnr = options.buffer 485 | end 486 | local mode, mapping = key:match("^(.)(.+)$") 487 | if not mode then 488 | assert(false, "nvim_apply_mappings: invalid mode specified for keymapping "..key) 489 | end 490 | if not valid_modes[mode] then 491 | assert(false, "nvim_apply_mappings: invalid mode specified for keymapping. mode="..mode) 492 | end 493 | mode = valid_modes[mode] 494 | local rhs = options[1] 495 | -- Remove this because we're going to pass it straight to nvim_set_keymap 496 | options[1] = nil 497 | if type(rhs) == 'function' then 498 | -- Use a value that won't be misinterpreted below since special keys 499 | -- like can be in key, and escaping those isn't easy. 500 | local escaped = escape_keymap(key) 501 | local key_mapping 502 | if options.dot_repeat then 503 | local key_function = rhs 504 | rhs = function() 505 | key_function() 506 | -- -- local repeat_expr = key_mapping 507 | -- local repeat_expr = mapping 508 | -- repeat_expr = api.nvim_replace_termcodes(repeat_expr, true, true, true) 509 | -- nvim.fn["repeat#set"](repeat_expr, nvim.v.count) 510 | nvim.fn["repeat#set"](nvim.replace_termcodes(key_mapping, true, true, true), nvim.v.count) 511 | end 512 | options.dot_repeat = nil 513 | end 514 | if options.buffer then 515 | -- Initialize and establish cleanup 516 | if not LUA_BUFFER_MAPPING[bufnr] then 517 | LUA_BUFFER_MAPPING[bufnr] = {} 518 | -- Clean up our resources. 519 | api.nvim_buf_attach(bufnr, false, { 520 | on_detach = function() 521 | LUA_BUFFER_MAPPING[bufnr] = nil 522 | end 523 | }) 524 | end 525 | LUA_BUFFER_MAPPING[bufnr][escaped] = rhs 526 | -- TODO HACK figure out why doesn't work in visual mode. 527 | if mode == "x" or mode == "v" then 528 | key_mapping = (":lua LUA_BUFFER_MAPPING[%d].%s()"):format(bufnr, escaped) 529 | else 530 | key_mapping = ("lua LUA_BUFFER_MAPPING[%d].%s()"):format(bufnr, escaped) 531 | end 532 | else 533 | LUA_MAPPING[escaped] = rhs 534 | -- TODO HACK figure out why doesn't work in visual mode. 535 | if mode == "x" or mode == "v" then 536 | key_mapping = (":lua LUA_MAPPING.%s()"):format(escaped) 537 | else 538 | key_mapping = ("lua LUA_MAPPING.%s()"):format(escaped) 539 | end 540 | end 541 | rhs = key_mapping 542 | options.noremap = true 543 | options.silent = true 544 | end 545 | if options.buffer then 546 | options.buffer = nil 547 | api.nvim_buf_set_keymap(bufnr, mode, mapping, rhs, options) 548 | else 549 | api.nvim_set_keymap(mode, mapping, rhs, options) 550 | end 551 | end 552 | end 553 | 554 | function nvim_create_augroups(definitions) 555 | for group_name, definition in pairs(definitions) do 556 | api.nvim_command('augroup '..group_name) 557 | api.nvim_command('autocmd!') 558 | for _, def in ipairs(definition) do 559 | -- if type(def) == 'table' and type(def[#def]) == 'function' then 560 | -- def[#def] = lua_callback(def[#def]) 561 | -- end 562 | local command = table.concat(vim.tbl_flatten{'autocmd', def}, ' ') 563 | api.nvim_command(command) 564 | end 565 | api.nvim_command('augroup END') 566 | end 567 | end 568 | 569 | --- Highlight a region in a buffer from the attributes specified 570 | function nvim_highlight_region(buf, ns, highlight_name, 571 | region_line_start, region_byte_start, 572 | region_line_end, region_byte_end) 573 | if region_line_start == region_line_end then 574 | api.nvim_buf_add_highlight(buf, ns, highlight_name, region_line_start, region_byte_start, region_byte_end) 575 | else 576 | api.nvim_buf_add_highlight(buf, ns, highlight_name, region_line_start, region_byte_start, -1) 577 | for linenum = region_line_start + 1, region_line_end - 1 do 578 | api.nvim_buf_add_highlight(buf, ns, highlight_name, linenum, 0, -1) 579 | end 580 | api.nvim_buf_add_highlight(buf, ns, highlight_name, region_line_end, 0, region_byte_end) 581 | end 582 | end 583 | 584 | 585 | --- 586 | -- Things Lua should've had 587 | --- 588 | 589 | function string.startswith(s, n) 590 | return s:sub(1, #n) == n 591 | end 592 | 593 | function string.endswith(self, str) 594 | return self:sub(-#str) == str 595 | end 596 | 597 | --- 598 | -- SPAWN UTILS 599 | --- 600 | 601 | local function clean_handles() 602 | local n = 1 603 | while n <= #HANDLES do 604 | if HANDLES[n]:is_closing() then 605 | table.remove(HANDLES, n) 606 | else 607 | n = n + 1 608 | end 609 | end 610 | end 611 | 612 | HANDLES = {} 613 | 614 | function spawn(cmd, params, onexit) 615 | local handle, pid 616 | handle, pid = vim.loop.spawn(cmd, params, function(code, signal) 617 | if type(onexit) == 'function' then onexit(code, signal) end 618 | handle:close() 619 | clean_handles() 620 | end) 621 | table.insert(HANDLES, handle) 622 | return handle, pid 623 | end 624 | 625 | --- MISC UTILS 626 | 627 | function epoch_ms() 628 | local s, ns = vim.loop.gettimeofday() 629 | return s * 1000 + math.floor(ns / 1000) 630 | end 631 | 632 | function epoch_ns() 633 | local s, ns = vim.loop.gettimeofday() 634 | return s * 1000000 + ns 635 | end 636 | 637 | 638 | --------------------------------------------------------------------------------