--- mod-version:3 local core = require "core" local style = require "core.style" local command = require "core.command" local keymap = require "core.keymap" local DocView = require "core.docview" local config = require "core.config" local common = require "core.common" -- Colors can be configured as follows: -- underline color = `style.bracketmatch_color` -- bracket color = `style.bracketmatch_char_color` -- background color = `style.bracketmatch_block_color` -- frame color = `style.bracketmatch_frame_color` config.plugins.bracketmatch = common.merge({ -- highlight the current bracket too highlight_both = true, -- can be "underline", "block", "frame", "none" style = "underline", -- color the bracket color_char = false, -- the size of the lines used in "underline" and "frame" line_size = math.ceil(1 * SCALE), -- The config specification used by the settings gui config_spec = { name = "Bracket Match", { label = "Highlight Both", description = "Highlight the current bracket too.", path = "highlight_both", type = "toggle", default = true }, { label = "Style", description = "The visual indicator for pair brackets.", path = "style", type = "selection", default = "underline", values = { {"Underline", "underline"}, {"Block", "block"}, {"Frame", "frame"}, {"None", "none"} } }, { label = "Colorize Bracket", description = "Change the color of the matching brackets.", path = "color_char", type = "toggle", default = false }, { label = "Line Size", description = "Height of the underline on matching brackets.", path = "line_size", type = "number", default = 1, min = 1, step = 1, get_value = function(value) return math.floor(value / SCALE) end, set_value = function(value) return math.ceil(value * SCALE) end } } }, config.plugins.bracketmatch) local bracket_maps = { -- [ ] ( ) { } { [91] = 93, [40] = 41, [123] = 125, direction = 1 }, -- ] [ ) ( } { { [93] = 91, [41] = 40, [125] = 123, direction = -1 }, } local function get_token_at(doc, line, col) local column = 0 for _,type,text in doc.highlighter:each_token(line) do column = column + #text if column >= col then return type, text end end end local function get_matching_bracket(doc, line, col, line_limit, open_byte, close_byte, direction) local end_line = line + line_limit * direction local depth = 0 while line ~= end_line do local byte = doc.lines[line]:byte(col) if byte == open_byte and get_token_at(doc, line, col) ~= "comment" then depth = depth + 1 elseif byte == close_byte and get_token_at(doc, line, col) ~= "comment" then depth = depth - 1 if depth == 0 then return line, col end end local prev_line, prev_col = line, col line, col = doc:position_offset(line, col, direction) if line == prev_line and col == prev_col then break end end end local state = {} local select_adj = 0 local function update_state(line_limit) line_limit = line_limit or math.huge -- reset if we don't have a document (eg. DocView isn't focused) local doc = core.active_view.doc if not doc then state = {} return end -- early exit if nothing has changed since the last call local line, col = doc:get_selection() local change_id = doc:get_change_id() if state.doc == doc and state.line == line and state.col == col and state.change_id == change_id and state.limit == line_limit then return end -- find matching bracket if we're on a bracket local line2, col2 for _, map in ipairs(bracket_maps) do for i = 0, -1, -1 do local line, col = doc:position_offset(line, col, i) local open = doc.lines[line]:byte(col) local close = map[open] if close and get_token_at(doc, line, col) ~= "comment" then -- i == 0 if the cursor is on the left side of a bracket (or -1 when on right) select_adj = i + 1 -- if i == 0 then select_adj = 1 else select_adj = 0 end line2, col2 = get_matching_bracket(doc, line, col, line_limit, open, close, map.direction) goto found end end end ::found:: -- update state = { change_id = change_id, doc = doc, line = line, col = col, line2 = line2, col2 = col2, limit = line_limit, } end local update = DocView.update function DocView:update(...) update(self, ...) update_state(100) end local function redraw_char(dv, x, y, line, col, bg_color, char_color) local x1 = x + dv:get_col_x_offset(line, col) local x2 = x + dv:get_col_x_offset(line, col + 1) local lh = dv:get_line_height() local token = get_token_at(dv.doc, line, col) if not char_color then char_color = style.syntax[token] end local font = style.syntax_fonts[token] or dv:get_font() local char = string.sub(dv.doc.lines[line], col, col) if not bg_color then -- redraw background core.push_clip_rect(x1, y, x2 - x1, lh) local dlt = DocView.draw_line_text DocView.draw_line_text = function() end dv:draw_line_body(line, x, y) DocView.draw_line_text = dlt core.pop_clip_rect() else renderer.draw_rect(x1, y, x2 - x1, lh, bg_color) end renderer.draw_text(font, char, x1, y + dv:get_line_text_y_offset(), char_color) end local function draw_decoration(dv, x, y, line, col) local conf = config.plugins.bracketmatch local color = style.bracketmatch_color or style.syntax["function"] local char_color = style.bracketmatch_char_color or (conf.style == "block" and style.background or style.syntax["keyword"]) local block_color = style.bracketmatch_block_color or style.line_number2 local frame_color = style.bracketmatch_frame_color or style.line_number2 local h = conf.line_size if conf.color_char or conf.style == "block" then redraw_char(dv, x, y, line, col, conf.style == "block" and block_color, conf.color_char and char_color) end if conf.style == "underline" then local x1 = x + dv:get_col_x_offset(line, col) local x2 = x + dv:get_col_x_offset(line, col + 1) local lh = dv:get_line_height() renderer.draw_rect(x1, y + lh - h, x2 - x1, h, color) elseif conf.style == "frame" then local x1 = x + dv:get_col_x_offset(line, col) local x2 = x + dv:get_col_x_offset(line, col + 1) local lh = dv:get_line_height() renderer.draw_rect(x1, y + lh - h, x2 - x1, h, frame_color) renderer.draw_rect(x1, y, x2 - x1, h, frame_color) renderer.draw_rect(x1, y, h, lh, frame_color) renderer.draw_rect(x2, y, h, lh, frame_color) end end local draw_line_text = DocView.draw_line_text function DocView:draw_line_text(line, x, y) local lh = draw_line_text(self, line, x, y) if self.doc == state.doc and state.line2 then if line == state.line2 then draw_decoration(self, x, y, line, state.col2) end if line == state.line and config.plugins.bracketmatch.highlight_both then draw_decoration(self, x, y, line, state.col + select_adj - 1) end end return lh end command.add("core.docview", { ["bracket-match:move-to-matching"] = function(dv) update_state() if state.line2 then dv.doc:set_selection(state.line2, state.col2) end end, ["bracket-match:select-to-matching"] = function(dv) update_state() if state.line2 then dv.doc:set_selection(state.line, state.col, state.line2, state.col2 + select_adj) end end, }) keymap.add { ["ctrl+m"] = "bracket-match:move-to-matching", ["ctrl+shift+m"] = "bracket-match:select-to-matching", }