Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 92 additions & 26 deletions lua/strict-paredit/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,85 @@ local symmetric_delims = {
['"'] = true,
}

-- Common string/comment node types across languages
local skip_types = {
"string",
"string_content",
"str_lit",
"string_literal",
"comment",
"line_comment",
"block_comment",
"regex",
"regex_lit",
}

-- Check if a treesitter node is (or is inside) a string/comment
local function in_string_or_comment_at(bufnr, row, col)
local ok, node = pcall(vim.treesitter.get_node, { bufnr = bufnr, pos = { row, col } })
if not ok or not node then
return false
end

local current = node
while current do
local node_type = current:type()
for _, t in ipairs(skip_types) do
if node_type == t or node_type:match(t) then
return true
end
end

current = current:parent()
end

return false
end

-- Helper: current buffer balance for a given opening delimiter
-- Returns positive when there are more opens than closes, zero when balanced,
-- and negative when there are more closes (i.e. an unmatched closing exists).
local function delimiter_balance(open_char)
local close_char = opening_delims[open_char]
if not close_char then
return 0
end

local bufnr = vim.api.nvim_get_current_buf()
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local open_count, close_count = 0, 0

for row, line in ipairs(lines) do
local search_start = 1
while true do
local idx = line:find(vim.pesc(open_char), search_start, true)
if not idx then
break
end
local col = idx - 1 -- zero-indexed for treesitter
if not in_string_or_comment_at(bufnr, row - 1, col) then
open_count = open_count + 1
end
search_start = idx + 1
end

search_start = 1
while true do
local idx = line:find(vim.pesc(close_char), search_start, true)
if not idx then
break
end
local col = idx - 1
if not in_string_or_comment_at(bufnr, row - 1, col) then
close_count = close_count + 1
end
search_start = idx + 1
end
end

return open_count - close_count
end

-- Helper: get character at specific buffer position
local function get_char_at(bufnr, row, col)
local line = vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1]
Expand Down Expand Up @@ -149,12 +228,24 @@ end
-- Handle opening delimiter in insert mode
local function handle_open_insert(char)
local closing = opening_delims[char]

-- If the buffer already has more closing than opening delimiters of this
-- type, we are fixing an existing imbalance; don't add another closing.
if delimiter_balance(char) < 0 then
return char
end
-- Always auto-pair: insert both and place cursor between
return char .. closing .. "<Left>"
end

-- Handle closing delimiter in insert mode
local function handle_close_insert(char)
local open = closing_delims[char]
-- Allow typing a needed closing delimiter when there are more opens than closes
if delimiter_balance(open) > 0 then
return char
end

local at_cursor = char_at_cursor()
if at_cursor == char then
-- Move over existing closing delimiter
Expand All @@ -172,32 +263,7 @@ local function in_string_or_comment()
local cursor = vim.api.nvim_win_get_cursor(0)
local row, col = cursor[1] - 1, cursor[2]

local ok, node = pcall(vim.treesitter.get_node, { bufnr = bufnr, pos = { row, col } })
if not ok or not node then
return false
end

local node_type = node:type()
-- Common string/comment node types across languages
local skip_types = {
"string",
"string_content",
"str_lit",
"string_literal",
"comment",
"line_comment",
"block_comment",
"regex",
"regex_lit",
}

for _, t in ipairs(skip_types) do
if node_type == t or node_type:match(t) then
return true
end
end

return false
return in_string_or_comment_at(bufnr, row, col)
end

-- Handle symmetric delimiter (like ") in insert mode
Expand Down