├── LICENSE.md ├── README.md ├── lua └── indent-o-matic.lua └── plugin └── indent-o-matic.vim /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Naqua Darazaki 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # indent-o-matic 2 | 3 | Dumb automatic fast indentation detection for Neovim written in Lua 4 | 5 | ## How it works 6 | 7 | Instead of trying to be smart about detecting an indentation using statistics, 8 | it will find the first thing that looks like a standard indentation (tab or 8/4/2 spaces) 9 | and assume that's what the file's indentation is 10 | 11 | This has the advantage of being fast and very often correct while being simple enough 12 | that most people will understand what it will do predictably 13 | 14 | ## Requirements 15 | 16 | - Neovim >= 0.4.4 17 | 18 | ## Installation 19 | 20 | Can be installed through any standard Vim package manager, configuration is optional 21 | 22 | E.g. through [vim-plug](https://github.com/junegunn/vim-plug): 23 | 24 | ```vim 25 | call plug#begin() 26 | Plug 'Darazaki/indent-o-matic' 27 | call plug#end() 28 | ``` 29 | 30 | Then restart Neovim and run `:PlugInstall` 31 | 32 | ## Configuration 33 | 34 | Configuration is done in Lua: 35 | 36 | ```lua 37 | require('indent-o-matic').setup { 38 | -- The values indicated here are the defaults 39 | 40 | -- Number of lines without indentation before giving up (use -1 for infinite) 41 | max_lines = 2048, 42 | 43 | -- Space indentations that should be detected 44 | standard_widths = { 2, 4, 8 }, 45 | 46 | -- Skip multi-line comments and strings (more accurate detection but less performant) 47 | skip_multiline = true, 48 | } 49 | ``` 50 | 51 | You can also directly configure it from a Vim file by using the `lua` instruction: 52 | 53 | ```vim 54 | lua < no strings/comments 94 | return false 95 | end 96 | 97 | local root = ts_utils.get_root_for_position(line_number, 0, root_lang_tree) 98 | if not root then 99 | -- No syntax tree on this line 100 | return false 101 | end 102 | 103 | -- Get the node's type for the first character of the line 104 | local node = root:named_descendant_for_range(line_number, 0, line_number, 0) 105 | local node_type = node:type() 106 | 107 | return node_type == 'comment' or node_type == 'string' 108 | end 109 | 110 | -- Get the correct `is_multiline` function based on the current buffer's configuration 111 | local function get_is_multiline_function() 112 | local buf = vim.api.nvim_get_current_buf() 113 | 114 | if ts_enabled and ts_highlighter.active[buf] then 115 | -- Buffer is highlighted through tree-sitter 116 | return is_multiline_ts 117 | else 118 | -- Default fallback 119 | return is_multiline_syn 120 | end 121 | end 122 | 123 | -- Configure the plugin 124 | function M.setup(options) 125 | if type(options) == 'table' then 126 | preferences = options 127 | else 128 | local msg = "Can't setup indent-o-matic, correct syntax is: " 129 | msg = msg .. "require('indent-o-matic').setup { ... }" 130 | error(msg) 131 | end 132 | end 133 | 134 | -- Attempt to detect current buffer's indentation and apply it to local settings 135 | function M.detect() 136 | local default = get_default_indent() 137 | local detected = default 138 | 139 | -- Options 140 | local max_lines = config('max_lines', 2048) 141 | local standard_widths = config('standard_widths', { 2, 4, 8 }) 142 | local skip_multiline = config('skip_multiline', true) 143 | 144 | -- Figure out the maximum space indentation possible 145 | table.sort(standard_widths) 146 | local max_indentation 147 | if #standard_widths == 0 then 148 | max_indentation = 0 149 | else 150 | max_indentation = standard_widths[#standard_widths] 151 | end 152 | 153 | -- Detect which method to use to detect multiline strings and comments 154 | local is_multiline 155 | if skip_multiline then 156 | is_multiline = get_is_multiline_function() 157 | end 158 | 159 | -- Loop over every line, breaking once it finds something that looks like a 160 | -- standard indentation or if it reaches end of file 161 | local i = 0 162 | while i ~= max_lines do 163 | local first_char 164 | 165 | local ok, line = pcall(function() return line_at(i) end) 166 | if not ok or line == nil then 167 | -- End of file 168 | break 169 | end 170 | 171 | -- Skip empty lines 172 | if not line or #line == 0 then 173 | goto continue 174 | end 175 | 176 | -- If a line starts with a tab then the file must be tab indented 177 | -- else if it starts with spaces it tries to detect if it's the file's indentation 178 | first_char = line:sub(1, 1) 179 | if first_char == '\t' then 180 | -- Skip multi-line comments and strings (1-indexed) 181 | if skip_multiline and is_multiline(i + 1) then 182 | goto continue 183 | end 184 | 185 | detected = 0 186 | break 187 | elseif first_char == ' ' then 188 | -- Figure out the number of spaces used and if it should be the indentation 189 | local j = 2 190 | while j ~= #line and j < max_indentation + 2 do 191 | local c = line:sub(j, j) 192 | if c == '\t' then 193 | -- Spaces and then a tab? WTF? Ignore this unholy line 194 | goto continue 195 | elseif c ~= ' ' then 196 | break 197 | end 198 | 199 | j = j + 1 200 | end 201 | 202 | -- If it's a standard number of spaces it's probably the file's indentation 203 | j = j - 1 204 | if contains(standard_widths, j) then 205 | -- Skip multi-line comments and strings (1-indexed) 206 | if skip_multiline and is_multiline(i + 1) then 207 | goto continue 208 | end 209 | 210 | detected = j 211 | break 212 | end 213 | end 214 | 215 | -- "We have continue at home" 216 | ::continue:: 217 | i = i + 1 218 | end 219 | 220 | if detected ~= default then 221 | if detected == 0 then 222 | setopt('expandtab', false) 223 | else 224 | setopt('expandtab', true) 225 | setopt('tabstop', detected) 226 | setopt('softtabstop', detected) 227 | setopt('shiftwidth', detected) 228 | end 229 | end 230 | end 231 | 232 | return M 233 | -------------------------------------------------------------------------------- /plugin/indent-o-matic.vim: -------------------------------------------------------------------------------- 1 | command! IndentOMatic execute "lua require('indent-o-matic').detect()" 2 | 3 | augroup indent_o_matic 4 | au! 5 | au BufReadPost * IndentOMatic 6 | " Run once when saving for new files 7 | au BufNewFile,BufAdd * au BufWritePost ++once IndentOMatic 8 | augroup END 9 | --------------------------------------------------------------------------------