├── LICENSE ├── README.md ├── dev └── init.lua ├── lua └── tablemd │ ├── init.lua │ ├── string_utils.lua │ ├── table_utils.lua │ └── tablemd.lua ├── plugin └── tablemd.vim └── test └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 allen-mack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim-table-md 2 | 3 | Neovim plugin to help with markdown tables. 4 | 5 | ## Key Maps 6 | 7 | | Key | Behavior | 8 | | --- | --- | 9 | | \tc | Add column to the right of the cursor | 10 | | \td | Delete the current column | 11 | | \tf | Format the table | 12 | | \tR | Add row above the current line | 13 | | \tr | Add row below the current line | 14 | | \tj | Align column to the left | 15 | | \tk | Center column | 16 | | \tl | Align column to the right | 17 | 18 | Until I can figure out how to assign these keybindings only to markdown files, just add the following to your `after/ftplugin/markdown.lua` file. Of course, you can change these to whatever keybindings you like. 19 | 20 | ``` 21 | vim.api.nvim_set_keymap("n", "tf", ':lua require("tablemd").format()', { noremap = true }) 22 | vim.api.nvim_set_keymap("n", "tc", ':lua require("tablemd").insertColumn(false)', { noremap = true }) 23 | vim.api.nvim_set_keymap("n", "td", ':lua require("tablemd").deleteColumn()', { noremap = true }) 24 | vim.api.nvim_set_keymap("n", "tr", ':lua require("tablemd").insertRow(false)', { noremap = true }) 25 | vim.api.nvim_set_keymap("n", "tR", ':lua require("tablemd").insertRow(true)', { noremap = true }) 26 | vim.api.nvim_set_keymap("n", "tj", ':lua require("tablemd").alignColumn("left")', { noremap = true }) 27 | vim.api.nvim_set_keymap("n", "tk", ':lua require("tablemd").alignColumn("center")', { noremap = true }) 28 | vim.api.nvim_set_keymap("n", "tl", ':lua require("tablemd").alignColumn("right")', { noremap = true }) 29 | ``` 30 | 31 | ## Developing 32 | 33 | There is an init file in the dev directory. If you source it like `:luafile dev/init.lua`, it will add a keymap that will reload the modules so that you can test your changes without having to exit and re-enter Neovim. 34 | 35 | ### Setting up the project 36 | 37 | - Create GitHub project 38 | - Clone the project 39 | - Open Neovim for editing\ 40 | *Note:* Make sure to add current directory to runtime path. Otherwise, Novim does not know how to find you plugin.\ 41 | `nvim --cmd "set rtp+=."` 42 | - Create Lua module in Lua source directory 43 | - Create init file and sub modules for the module\ 44 | `touch lua/greetings/init.lua` & `touch lua/greetings/awesome-module.lua` 45 | -------------------------------------------------------------------------------- /dev/init.lua: -------------------------------------------------------------------------------- 1 | package.loaded['tablemd'] = nil 2 | package.loaded['tablemd.tablemd'] = nil 3 | package.loaded['tablemd/string_utils'] = nil 4 | package.loaded['tablemd/table_utils'] = nil 5 | -- package.loaded['utils'] = nil 6 | -- package.loaded['tablemd.utils'] = nil 7 | package.loaded['dev'] = nil 8 | 9 | vim.api.nvim_set_keymap('n', ',r', ':luafile dev/init.lua', {}) 10 | 11 | TableMD = require('tablemd') 12 | 13 | -------------------------------------------------------------------------------- /lua/tablemd/init.lua: -------------------------------------------------------------------------------- 1 | local table_md = require('tablemd.tablemd') 2 | 3 | --[[ MAP KEYS ]]-- 4 | -- vim.api.nvim_set_keymap("n", "tf", ':lua require("tablemd").format()', { noremap = true }) 5 | -- vim.api.nvim_set_keymap("n", "tv", ':lua require("tablemd").insertColumn(false)', { noremap = true }) 6 | -- vim.api.nvim_set_keymap("n", "tc", ':lua require("tablemd").insertColumn(true)', { noremap = true }) 7 | -- vim.api.nvim_set_keymap("n", "td", ':lua require("tablemd").deleteColumn()', { noremap = true }) 8 | -- vim.api.nvim_set_keymap("n", "tr", ':lua require("tablemd").insertRow(false)', { noremap = true }) 9 | -- vim.api.nvim_set_keymap("n", "tR", ':lua require("tablemd").insertRow(true)', { noremap = true }) 10 | 11 | return { 12 | format = table_md.formatTable, 13 | help = table_md.help, 14 | alignColumn = table_md.alignColumn, 15 | deleteColumn = table_md.deleteColumn, 16 | insertColumn = table_md.insertColumn, 17 | insertRow = table_md.insertRow 18 | } 19 | -------------------------------------------------------------------------------- /lua/tablemd/string_utils.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | --[[-- 4 | Pad string 5 | @tparam string input The string to pad. 6 | @tparam int len The expected length of the return value. 7 | @treturn string String padded with spaces. 8 | ]] 9 | function M.pad_string(input, len, alignment) 10 | -- Treat alignment as an optional paramenter with a default value 11 | alignment = alignment or 'left' 12 | 13 | local space_count = len - string.len(input); 14 | 15 | -- default the spacing to a left justification 16 | local left_spaces = 0 17 | local right_spaces = space_count 18 | 19 | -- if the alignment is 'right' then put all the space on the left side of the string. 20 | if alignment == 'right' then 21 | left_spaces = space_count 22 | right_spaces = 0 23 | end 24 | 25 | -- If the alignment is 'center', split the space between the left and right. 26 | -- For uneven splits, put the extra space on the right side of the string. 27 | if alignment == 'center' then 28 | if space_count > 0 then 29 | left_spaces = math.floor(space_count / 2) 30 | right_spaces = space_count - left_spaces 31 | end 32 | end 33 | 34 | input = string.rep(" ", left_spaces) .. input .. string.rep(" ", right_spaces) 35 | 36 | return input 37 | end 38 | 39 | --[[-- 40 | Split string 41 | @tparam string input The string to split. 42 | @tparam string sep The separator string. 43 | @return table Table containing the split pieces 44 | ]] 45 | function M.split_string(input, sep) 46 | if sep == nil then 47 | sep = "%s" 48 | end 49 | 50 | local t = {} 51 | for str in string.gmatch(input, "([^"..sep.."]+)") do 52 | table.insert(t, str) 53 | end 54 | return t 55 | end 56 | 57 | --[[-- 58 | Trim spaces from beginning and end of string 59 | @tparam string s The string to trim. 60 | @treturn string The trimmed string 61 | ]] 62 | function M.trim_string(s) 63 | return s:match( "^%s*(.-)%s*$" ) 64 | end 65 | 66 | -- Export module 67 | return M 68 | -------------------------------------------------------------------------------- /lua/tablemd/table_utils.lua: -------------------------------------------------------------------------------- 1 | local su = require('tablemd/string_utils') 2 | 3 | local M = {} 4 | 5 | --[[-- 6 | Returns a table with the max column widths and alignment information. 7 | @tparam int s The first line of the table 8 | @tparam int e The last line of the table 9 | @treturn table Table with information for each column 10 | ]] 11 | function M.get_column_defs(s, e) 12 | -- look for alignment clues on the second line 13 | local second_line = s + 1 14 | local defs = {} 15 | 16 | for i = s, e do 17 | -- Read the line from the buffer 18 | -- local line = vim.api.nvim_buf_get_lines(0, i-1, i, false)[1] 19 | local line = su.trim_string(vim.api.nvim_buf_get_lines(0, i-1, i, false)[1]) 20 | 21 | -- Split the line by the pipe symbol 22 | local t = su.split_string(line, "|") 23 | 24 | -- For each column 25 | for k, v in ipairs(t) do 26 | local trimmed_str = su.trim_string(v) 27 | local str_len = string.len(trimmed_str) 28 | local alignment = nil 29 | 30 | -- look for alignment indicators 31 | if string.match(trimmed_str, "^-+:$") then 32 | alignment = 'right' 33 | elseif string.match(trimmed_str, "^:[-]+:$") then 34 | alignment = 'center' 35 | elseif string.match(trimmed_str, "^:-+$") or string.match(trimmed_str, "^-+$") then 36 | alignment = 'left' 37 | else 38 | alignment = nil 39 | end 40 | 41 | -- if the sub table doesn't already exist, then add it 42 | if defs[k] == nil then 43 | table.insert(defs, k, {length = str_len, align = alignment}) 44 | else 45 | -- update the object if the length is greater 46 | if str_len > defs[k]["length"] then 47 | defs[k]["length"] = str_len 48 | end 49 | -- if we haven't already set the alignment 50 | if defs[k]["align"] == nil then 51 | defs[k]["align"] = alignment 52 | end 53 | end 54 | end 55 | end 56 | 57 | return defs 58 | end 59 | 60 | --[[-- 61 | Determines which column the cursor is currently in. 62 | @treturn int The column index. This is Lua, so it is 1 based. 63 | ]] 64 | function M.get_current_column_index() 65 | local cursor_location = vim.api.nvim_win_get_cursor(0) 66 | local line = su.trim_string(vim.api.nvim_buf_get_lines(0, cursor_location[1]-1, cursor_location[1], false)[1]) 67 | line = string.sub(line, 1, cursor_location[2]-1) 68 | 69 | local count = 0 70 | for i in line:gmatch("|") do 71 | count = count + 1 72 | end 73 | 74 | return count 75 | end 76 | 77 | --[[-- 78 | Returns the formatted line 79 | @tparam string line The line to be formatted 80 | @tparam table col_defs Table with metadata about each column 81 | @treturn string The formatted replacement line 82 | ]] 83 | function M.get_formatted_line(line, col_defs) 84 | local t = su.split_string(line, "|") 85 | local build_str = "| " 86 | 87 | for k, v in ipairs(t) do 88 | local col_width = col_defs[k]["length"] 89 | local col_align = col_defs[k]["align"] 90 | local padded_str = su.pad_string(su.trim_string(v), col_width, col_align) 91 | build_str = build_str .. padded_str .. " | " 92 | end 93 | 94 | -- Trim off beginning or trailing spaces. 95 | build_str = string.gsub(build_str, '^%s*(.-)%s*$', '%1') 96 | 97 | return build_str 98 | end 99 | 100 | --[[-- 101 | Find the first line and last line of the table. 102 | @tparam int current_line_number The line number that the cursor is currently on. 103 | @treturn int, int The start line and end line for the table. 104 | ]] 105 | function M.get_table_range(current_line_number) 106 | local start_line 107 | local end_line 108 | local current_line = nil 109 | local buf_line_count = vim.api.nvim_buf_line_count(0) 110 | 111 | -- Go Up 112 | start_line = current_line_number -- - 1 113 | 114 | repeat 115 | current_line = vim.api.nvim_buf_get_lines(0, start_line-1, start_line, false)[1] 116 | start_line = start_line - 1 117 | until current_line == "" or start_line == 0 118 | 119 | start_line = start_line + 2 120 | 121 | -- Go down 122 | end_line = current_line_number --+ 1 123 | current_line = vim.api.nvim_buf_get_lines(0, end_line-1, end_line, false)[1] 124 | 125 | while current_line ~= "" and current_line ~= nil and end_line <= buf_line_count do 126 | end_line = end_line + 1 127 | current_line = vim.api.nvim_buf_get_lines(0, end_line-1, end_line, false)[1] 128 | end 129 | 130 | end_line = end_line - 1 131 | 132 | -- Return start and end line numbers 133 | return start_line, end_line 134 | end 135 | 136 | -- Export Module 137 | return M 138 | -------------------------------------------------------------------------------- /lua/tablemd/tablemd.lua: -------------------------------------------------------------------------------- 1 | local su = require('tablemd/string_utils') 2 | local tu = require('tablemd/table_utils') 3 | 4 | local M = {} 5 | 6 | --[[ 7 | Formats the markdown table so cells in a column are a uniform width. 8 | ]] 9 | function M.formatTable() 10 | local start_line = nil 11 | local end_line = nil 12 | local cur_line = nil 13 | local cursor_location = vim.fn.line('v') 14 | 15 | -- Get the range of lines to format 16 | start_line, end_line = tu.get_table_range(cursor_location) 17 | 18 | -- Get column definitions 19 | local col_defs = tu.get_column_defs(start_line, end_line) 20 | 21 | -- Format each line 22 | for i = start_line, end_line do -- The range includes both ends. 23 | local line = su.trim_string(vim.api.nvim_buf_get_lines(0, i-1, i, false)[1]) 24 | local formatted_line = tu.get_formatted_line(line, col_defs) 25 | 26 | -- replace the line with the formatted line in the buffer 27 | vim.api.nvim_buf_set_lines(0, i-1, i, false, {formatted_line}) 28 | end 29 | end 30 | 31 | --[[-- 32 | Aligns the column. Possible values for alignment are "left", "right", and "center". 33 | ]] 34 | function M.alignColumn(alignment) 35 | -- Don't do anything if the alignment value isn't one of the predefined values. 36 | if not (alignment == "left" or alignment == "right" or alignment == "center") then 37 | return 38 | end 39 | 40 | local start_line = nil 41 | local end_line = nil 42 | local cursor_location = vim.api.nvim_win_get_cursor(0) 43 | 44 | -- Get the range of lines to format 45 | start_line, end_line = tu.get_table_range(cursor_location[1]) 46 | 47 | -- Get column definitions 48 | local col_defs = tu.get_column_defs(start_line, end_line) 49 | 50 | -- Get the current column index 51 | local current_col = tu.get_current_column_index() 52 | 53 | -- Get the second line 54 | local line = su.trim_string(vim.api.nvim_buf_get_lines(0, start_line, start_line+1, false)[1]) 55 | local t = su.split_string(line, "|") 56 | 57 | -- Rebuild the line 58 | local new_line = "|" 59 | local table_len = 0 60 | for _ in pairs(t) do table_len = table_len + 1 end 61 | 62 | for i = 1, table_len do 63 | if i ~= current_col then 64 | new_line = new_line .. su.trim_string(t[i]) .. " | " 65 | else 66 | if alignment == "left" then 67 | new_line = new_line .. "--- | " 68 | end 69 | 70 | if alignment == "right" then 71 | new_line = new_line .. "---: | " 72 | end 73 | 74 | if alignment == "center" then 75 | new_line = new_line .. ":---: | " 76 | end 77 | end 78 | end 79 | 80 | -- replace the line with the formatted line in the buffer 81 | vim.api.nvim_buf_set_lines(0, start_line, start_line+1, false, {new_line}) 82 | 83 | M.formatTable() 84 | end 85 | 86 | --[[-- 87 | Deletes the current column from the table. 88 | ]] 89 | function M.deleteColumn() 90 | local start_line = nil 91 | local end_line = nil 92 | local cursor_location = vim.api.nvim_win_get_cursor(0) 93 | 94 | -- Get the range of lines to format 95 | start_line, end_line = tu.get_table_range(cursor_location[1]) 96 | 97 | -- Get column definitions 98 | local col_defs = tu.get_column_defs(start_line, end_line) 99 | 100 | -- Get the current column index 101 | local current_col = tu.get_current_column_index() 102 | 103 | -- Format each line 104 | for i = start_line, end_line do 105 | local line = su.trim_string(vim.api.nvim_buf_get_lines(0, i-1, i, false)[1]) 106 | local t = su.split_string(line, "|") 107 | 108 | local new_line = "|" 109 | local table_len = 0 110 | for _ in pairs(t) do table_len = table_len + 1 end 111 | 112 | for j = 1, table_len do 113 | if j ~= current_col then 114 | new_line = new_line .. su.trim_string(t[j]) .. " | " 115 | end 116 | end 117 | 118 | -- replave the line with the formatted line in the buffer 119 | vim.api.nvim_buf_set_lines(0, i-1, i, false, {new_line}) 120 | end 121 | 122 | M.formatTable() 123 | end 124 | 125 | --[[-- 126 | Formats each line in the table with a new column. 127 | @tparam bool before If true, the column will be inserted on the left side of the current column 128 | ]] 129 | function M.insertColumn(before) 130 | local start_line = nil 131 | local end_line = nil 132 | local cursor_location = vim.api.nvim_win_get_cursor(0) 133 | 134 | -- Get the range of lines to format 135 | start_line, end_line = tu.get_table_range(cursor_location[1]) 136 | 137 | -- Get column definitions 138 | local col_defs = tu.get_column_defs(start_line, end_line) 139 | 140 | -- Get the current column index 141 | local current_col = tu.get_current_column_index() 142 | 143 | -- Format each line 144 | for i = start_line, end_line do -- The range includes both ends. 145 | local line = su.trim_string(vim.api.nvim_buf_get_lines(0, i-1, i, false)[1]) 146 | 147 | local t = su.split_string(line, "|") 148 | 149 | local new_line = "|" 150 | local table_len = 0 151 | for _ in pairs(t) do table_len = table_len + 1 end 152 | 153 | -- This code gets "duplicated" in the next loop, so it is encapsulated in a function here. 154 | local insertBlankColumn = function(new_line, start_line, i) 155 | if i == start_line + 1 then 156 | new_line = new_line .. "--- | " 157 | else 158 | new_line = new_line .. " | " 159 | end 160 | 161 | return new_line 162 | end 163 | 164 | -- Loop through every column in the table line. 165 | for j = 1, table_len do 166 | if before == true and j == current_col then 167 | new_line = insertBlankColumn(new_line, start_line, i) 168 | end 169 | 170 | new_line = new_line .. su.trim_string(t[j]) .. " | " 171 | 172 | if before == false and j == current_col then 173 | new_line = insertBlankColumn(new_line, start_line, i) 174 | end 175 | end 176 | 177 | -- replace the line with the formatted line in the buffer 178 | vim.api.nvim_buf_set_lines(0, i-1, i, false, {new_line}) 179 | end 180 | 181 | M.formatTable() 182 | end 183 | 184 | --[[-- 185 | Inserts a new row into the table 186 | @tparam bool before If true, the row will be inserted above the current row 187 | ]] 188 | function M.insertRow(before) 189 | -- Get the current location of the cursor 190 | local cursor_location = vim.api.nvim_win_get_cursor(0) 191 | print(vim.inspect(cursor_location)) 192 | local line_num = cursor_location[1] 193 | 194 | local col_defs = tu.get_column_defs(line_num, line_num) 195 | local new_line = "|" 196 | 197 | for k, v in ipairs(col_defs) do 198 | new_line = new_line .. " |" 199 | end 200 | 201 | -- If 'before' is true, then insert the row above the current row. 202 | if before then 203 | line_num = line_num - 1 204 | end 205 | 206 | -- To insert a line, pass in the same line number for both start and end. 207 | vim.api.nvim_buf_set_lines(0, line_num, line_num, false, {new_line}) 208 | 209 | -- Move the cursor to the newly created line 210 | vim.api.nvim_win_set_cursor(0, {line_num + 1, cursor_location[2]}) 211 | 212 | M.formatTable() 213 | end 214 | 215 | function M.setKeyMap() 216 | vim.api.nvim_set_keymap("n", "tf", ':lua require("tablemd").format()', { noremap = true }) 217 | vim.api.nvim_set_keymap("n", "tC", ':lua require("tablemd").insertColumn(false)', { noremap = true }) 218 | vim.api.nvim_set_keymap("n", "tc", ':lua require("tablemd").insertColumn(true)', { noremap = true }) 219 | vim.api.nvim_set_keymap("n", "td", ':lua require("tablemd").deleteColumn()', { noremap = true }) 220 | vim.api.nvim_set_keymap("n", "tr", ':lua require("tablemd").insertRow(false)', { noremap = true }) 221 | vim.api.nvim_set_keymap("n", "tR", ':lua require("tablemd").insertRow(true)', { noremap = true }) 222 | vim.api.nvim_set_keymap("n", "tk", ':lua require("tablemd").alignColumn("center")', { noremap = true }) 223 | end 224 | 225 | function M.help() 226 | print('THIS IS A HELP MESSAGE') 227 | end 228 | 229 | -- Export module 230 | return M 231 | 232 | -------------------------------------------------------------------------------- /plugin/tablemd.vim: -------------------------------------------------------------------------------- 1 | fun! Tablemd() 2 | lua for k in pairs(package.loaded) do if k:match("^nvim%-table%-md") then package.loaded[k] = nil end end 3 | lua require("tablemd") 4 | endfun 5 | 6 | augroup Tablemd 7 | autocmd! 8 | augroup END 9 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Try it out 2 | 3 | ## Format Table 4 | 5 | Here's a table that could look prettier. Use `tf` to format the table. 6 | 7 | | name | character homeworld | height | 8 | | --- | :---: | ---: | 9 | | Luke Skywalker | Tatooine | 172 | 10 | | C-3PO | Tatooine | 167 | 11 | | R2-D2 | Naboo | 96 | 12 | | Leia Organa | Alderaan | 150 | 13 | 14 | 15 | | name | character homeworld | height | 16 | | --- | :---: | ---: | 17 | | Luke Skywalker | Tatooine | 172 | 18 | | C-3PO | Tatooine | 167 | 19 | | R2-D2 | Naboo | 96 | 20 | | Leia Organa | Alderaan | 150 | 21 | --------------------------------------------------------------------------------