-- -- Cursor -- local Cursor = {} Cursor.__index = Cursor function Cursor.new(row, col) local cursor = {row = row, col = col} return setmetatable(cursor, Cursor) end function Cursor:is_valid() return self.row > 0 and self.col > 0 end function Cursor:is_string() assert(self:is_valid()) local syn_id = vim.fn.synID(self.row, self.col, false) local syn_attr = vim.fn.synIDattr(syn_id, 'name') return syn_attr:find('String$') end function Cursor.get_current() local _, row, col, _ = unpack(vim.fn.getpos('.')) return Cursor.new(row, col) end function Cursor:set_current() assert(self:is_valid()) vim.fn.setcursorcharpos({self.row, self.col}) end function Cursor.__eq(self, other) return self.row == other.row and self.col == other.col end -- -- BracePair -- local BracePair = {} BracePair.__index = BracePair function BracePair.new(open, close) local pair = {open = open, close = close} return setmetatable(pair, BracePair) end function BracePair.from_brace(brace) local all_pairs = { {'(', ')'}, {'[', ']'}, {'{', '}'}, {'<', '>'}, } for _, pair in ipairs(all_pairs) do if pair[1] == brace or pair[2] == brace then return BracePair.new(pair[1], pair[2]) end end end function BracePair:get_escaped() local escape_func = function(brace_raw) if brace_raw == '[' or brace_raw == ']' then return '\\' .. brace_raw else return brace_raw end end return BracePair.new( escape_func(self.open), escape_func(self.close) ) end function BracePair:find_closest(backward) -- See flags: https://neovim.io/doc/user/builtin.html#search() local flags = 'Wcn' if backward then flags = 'Wcnb' end local ignore_func = function() local cursor = Cursor.get_current() return cursor:is_string() end local escaped_pair = self:get_escaped() local position = vim.fn.searchpairpos( escaped_pair.open, '', escaped_pair.close, flags, ignore_func ) local brace_cursor = Cursor.new(unpack(position)) if brace_cursor:is_valid() then return brace_cursor end end function BracePair.__eq(self, other) return self.open == other.open and self.close == other.close end -- -- BraceStack -- local BraceStack = {} BraceStack.__index = BraceStack function BraceStack.new() local stack = {stack = {}} return setmetatable(stack, BraceStack) end function BraceStack:update(brace) local pair = BracePair.from_brace(brace) if pair then if brace == pair.close and self:top() == pair.open then self:pop() else self:push(brace) end end end function BraceStack:push(brace) table.insert(self.stack, brace) end function BraceStack:pop() assert(not self:empty()) return table.remove(self.stack, #self.stack) end function BraceStack:empty() return #self.stack == 0 end function BraceStack:top() return self.stack[#self.stack] end -- -- BraceRange -- local BraceRange = {} BraceRange.__index = BraceRange function BraceRange.new(start, stop, pair) local range = { start = start, stop = stop, pair = pair, } return setmetatable(range, BraceRange) end function BraceRange.find_closest(pair) local stop = pair:find_closest(false) if stop then local start = pair:find_closest(true) if start then return BraceRange.new(start, stop, pair) end end end function BraceRange.find_closest_any() local ranges = {} for _, brace in ipairs({'(', '[', '{', '<'}) do local pair = BracePair.from_brace(brace) local range = BraceRange.find_closest(pair) if range then table.insert(ranges, range) end end if #ranges > 0 then table.sort(ranges) return ranges[1] end end function BraceRange:is_wrapped() return self.start.row < self.stop.row end function BraceRange.__lt(range_1, range_2) local cursor = Cursor:get_current() local row_diff1 = range_1.start.row - cursor.row local col_diff1 = range_1.start.col - cursor.col local row_diff2 = range_2.start.row - cursor.row local col_diff2 = range_2.start.col - cursor.col if row_diff1 < row_diff2 then return false elseif row_diff1 > row_diff2 then return true elseif col_diff1 < col_diff2 then return false elseif col_diff1 > col_diff2 then return true else return true end end -- -- Param -- local Param = {} Param.__index = Param function Param.new(text, pair) local param = { text = text, pair = pair, offset = nil, } return setmetatable(param, Param) end function Param:append(char) self.text = self.text .. char end function Param:activate() self.offset = #self.text end function Param:is_active() return self.offset ~= nil end function Param:flush() if self.offset then self.offset = math.min(self.offset, #self.text:match('(.-)%s*$')) self.offset = math.max(self.offset - #self.text:match('^%s*'), 1) end self.text = self.text:match('^%s*(.-)%s*$') return #self.text > 0 end -- -- ParamList -- local ParamList = {} ParamList.__index = ParamList function ParamList.new(range) local params = { range = range, current = nil, parsed = {}, } return setmetatable(params, ParamList) end function ParamList:flush() if self.current then if self.current:flush() then table.insert(self.parsed, self.current) end self.current = nil end end function ParamList:update(char, brace_stack, cursor) if not cursor:is_string() then brace_stack:update(char) if brace_stack:empty() and char == ',' then self:flush() return end end if self.current then self.current:append(char) else self.current = Param.new(char, self.range) end if cursor == Cursor.get_current() then self.current:activate() end end function ParamList:parse() local brace_stack = BraceStack:new() for row = self.range.start.row, self.range.stop.row do local line = vim.fn.getline(row) local start_col = 1 if row == self.range.start.row then start_col = self.range.start.col + 1 end local stop_col = #line if row == self.range.stop.row then stop_col = self.range.stop.col - 1 end for col = start_col, stop_col do self:update(line:sub(col, col), brace_stack, Cursor.new(row, col)) end end self:flush() end -- -- Builder -- local Builder = {} Builder.__index = Builder function Builder.new(indent_level, indent_block) local builder = { lines = {}, line = '', indent_level = indent_level, indent_block = indent_block, } return setmetatable(builder, Builder) end function Builder:indent() self.indent_level = self.indent_level + 1 end function Builder:unindent() assert(self.indent_level > 0) self.indent_level = self.indent_level - 1 end function Builder:update(text) self.line = self.line .. text end function Builder:flush() local indent = string.rep(self.indent_block, self.indent_level) table.insert(self.lines, indent .. self.line) self.line = '' end function Builder:get_offset() local indent = string.rep(self.indent_block, self.indent_level) return Cursor.new(#self.lines, #self.line + #indent) end -- -- WrapContext -- local WrapContext = {} WrapContext.__index = WrapContext function WrapContext.new(opt) local wrap_context = { opt = opt, indent = '', prefix = '', suffix = '', range = nil, params = nil, } return setmetatable(wrap_context, WrapContext) end function WrapContext:parse() self.range = BraceRange.find_closest_any() if not self.range then return false end local first_line = vim.fn.getline(self.range.start.row) local indent = #first_line:match('^(%s*)') self.prefix = first_line:sub(indent + 1, self.range.start.col) if vim.o.expandtab then self.indent_level = indent / vim.o.shiftwidth self.indent_block = string.rep(' ', vim.o.shiftwidth) else self.indent_level = indent self.indent_block = '\t' end local last_line = vim.fn.getline(self.range.stop.row) self.suffix = last_line:sub(self.range.stop.col) self.params = ParamList.new(self.range) self.params:parse() return true end function WrapContext:wrap(opt) local builder = Builder.new(self.indent_level, self.indent_block) builder:update(self.prefix) builder:flush() builder:indent() local cursor = nil local update_param = function(param) if param:is_active() then cursor = builder:get_offset() cursor.row = cursor.row + self.range.start.row cursor.col = cursor.col + param.offset end builder:update(param.text) end for i, param in ipairs(self.params.parsed) do local is_first_param = i == 1 local is_last_param = i == #self.params.parsed if opt.comma_prefix then builder:update(opt.line_prefix) if not is_first_param then builder:update(', ') end update_param(param) else builder:update(opt.line_prefix) update_param(param) if not is_last_param or opt.comma_last then builder:update(',') end end if is_last_param and not opt.brace_last_wrap then builder:update(self.suffix) end builder:flush() end if not opt.brace_last_indent then builder:unindent() end if opt.brace_last_wrap then builder:update(self.suffix) end builder:flush() local row = self.range.start.row for i, line in ipairs(builder.lines) do if i == 1 then vim.fn.setline(row, line) else vim.fn.append(row, line) row = row + 1 end end if cursor then cursor:set_current() end end function WrapContext:unwrap(opt) local padding = '' if opt.brace_pad then padding = ' ' end local line = self.indent .. self.prefix .. padding local cursor = nil for i, param in ipairs(self.params.parsed) do if param:is_active() then cursor = Cursor.new(0, #line + param.offset) end line = line .. param.text if i < #self.params.parsed then line = line .. ', ' end end line = line .. padding .. self.suffix vim.fn.setline(self.range.start.row, line) vim.fn.execute(string.format('%d,%dd_', self.range.start.row + 1, self.range.stop.row)) for _, param in ipairs(self.params.parsed) do if param:is_active() then cursor.row = Cursor:get_current().row - 1 end end if cursor then cursor:set_current() end end function WrapContext:toggle() local opt = self:specialize_opt() if self.range:is_wrapped() then self:unwrap(opt) else self:wrap(opt) end end function WrapContext:specialize_opt() local opt = {} for key, value in pairs(self.opt) do if type(value) == 'table' then opt[key] = false for _, brace in ipairs(value) do if self.range.pair == BracePair.from_brace(brace) then opt[key] = true break end end else opt[key] = value end end return opt end return { WrapContext = WrapContext, }