From 5cfa03f2bdf312213b69cae329645f48da904ea1 Mon Sep 17 00:00:00 2001 From: Stephan Seitz Date: Mon, 20 Jul 2020 23:56:32 +0200 Subject: Textobjects: add swap feature --- lua/nvim-treesitter/configs.lua | 4 +- lua/nvim-treesitter/textobjects.lua | 120 +++++++++++++++++++++++++++++++++++- lua/nvim-treesitter/ts_utils.lua | 67 +++++++++++++++++--- 3 files changed, 179 insertions(+), 12 deletions(-) (limited to 'lua') diff --git a/lua/nvim-treesitter/configs.lua b/lua/nvim-treesitter/configs.lua index 02a636dff..350a8cdad 100644 --- a/lua/nvim-treesitter/configs.lua +++ b/lua/nvim-treesitter/configs.lua @@ -85,7 +85,9 @@ local builtin_modules = { is_supported = function(lang) return has_some_textobject_mapping(lang) or queries.has_textobjects(lang) end, - keymaps = {} + keymaps = {}, + swap_next_keymaps = {}, + swap_previous_keymaps = {} } } diff --git a/lua/nvim-treesitter/textobjects.lua b/lua/nvim-treesitter/textobjects.lua index 7227b9e0e..9c312f44a 100644 --- a/lua/nvim-treesitter/textobjects.lua +++ b/lua/nvim-treesitter/textobjects.lua @@ -8,7 +8,7 @@ local ts_utils = require'nvim-treesitter.ts_utils' local M = {} -function M.select_textobject(query_string) +local function get_textobject_at_point(query_string) local bufnr = vim.api.nvim_get_current_buf() local lang = parsers.get_buf_lang(bufnr) if not lang then return end @@ -66,11 +66,93 @@ function M.select_textobject(query_string) if smallest_range.start then local start_range = {smallest_range.start.node:range()} local node_range = {smallest_range.node:range()} - ts_utils.update_selection(bufnr, {start_range[1], start_range[2], node_range[3], node_range[4]}) + return bufnr, {start_range[1], start_range[2], node_range[3], node_range[4]}, smallest_range.node else - ts_utils.update_selection(bufnr, smallest_range.node) + return bufnr, {smallest_range.node:range()}, smallest_range.node + end + end +end + +function M.select_textobject(query_string) + local bufnr, textobject = get_textobject_at_point(query_string) + if textobject then + ts_utils.update_selection(bufnr, textobject) + end +end + +local function swap_textobject(query_string, direction) + local bufnr, textobject_range, node = get_textobject_at_point(query_string) + local step = direction > 0 and 1 or -1 + if not node then return end + for _ = 1, math.abs(direction), step do + if direction > 0 then + ts_utils.swap_nodes(textobject_range, M.next_textobject(node, query_string, true, bufnr), bufnr, "yes, set cursor!") + else + ts_utils.swap_nodes(textobject_range, M.previous_textobject(node, query_string, true, bufnr), bufnr, "yes, set cursor!") + end + end +end + +function M.swap_textobject_next(query_string) + swap_textobject(query_string, 1) +end + +function M.swap_textobject_previous(query_string) + swap_textobject(query_string, -1) +end + +function M.next_textobject(node, query_string, same_parent, bufnr) + local bufnr = bufnr or api.nvim_get_current_buf() + + local matches = queries.get_capture_matches(bufnr, query_string, 'textobjects') + local _, _ , node_end = node:end_() + local next_node + local next_node_start + + for _, m in pairs(matches) do + local _, _, other_end = m.node:start() + if other_end > node_end then + if not same_parent or node:parent() == m.node:parent() then + if not next_node then + next_node = m + _, _, next_node_start = next_node.node:start() + end + if other_end < next_node_start then + next_node = m + _, _, next_node_start = next_node.node:start() + end + end end end + + return next_node and next_node.node +end + +function M.previous_textobject(node, query_string, same_parent, bufnr) + local bufnr = bufnr or api.nvim_get_current_buf() + + local matches = queries.get_capture_matches(bufnr, query_string, 'textobjects') + local _, _ , node_start = node:start() + local previous_node + local previous_node_end + + for _, m in pairs(matches) do + local _, _, other_end = m.node:end_() + if other_end < node_start then + if not same_parent or node:parent() == m.node:parent() then + if not previous_node then + previous_node = m + _, _, previous_node_end = previous_node.node:end_() + end + if other_end > previous_node_end then + previous_node = m + _, _, previous_node_end = previous_node.node:end_() + end + end + end + end + + return previous_node and previous_node.node end function M.attach(bufnr, lang) @@ -90,6 +172,28 @@ function M.attach(bufnr, lang) api.nvim_buf_set_keymap(buf, "v", mapping, cmd, {silent = true, noremap = true }) end end + for mapping, query in pairs(config.swap_next_keymaps) do + if type(query) == 'table' then + query = query[lang] + elseif not queries.get_query(lang, 'textobjects') then + query = nil + end + if query then + local cmd = ":lua require'nvim-treesitter.textobjects'.swap_textobject_next('"..query.."')" + api.nvim_buf_set_keymap(buf, "n", mapping, cmd, {silent = true}) + end + end + for mapping, query in pairs(config.swap_previous_keymaps) do + if type(query) == 'table' then + query = query[lang] + elseif not queries.get_query(lang, 'textobjects') then + query = nil + end + if query then + local cmd = ":lua require'nvim-treesitter.textobjects'.swap_textobject_previous('"..query.."')" + api.nvim_buf_set_keymap(buf, "n", mapping, cmd, {silent = true}) + end + end end function M.detach(bufnr) @@ -108,6 +212,16 @@ function M.detach(bufnr) api.nvim_buf_del_keymap(buf, "v", mapping) end end + for mapping, query in pairs(config.swap_next_keymaps) or pairs(config.swap_previous_keymaps) do + if type(query) == 'table' then + query = query[lang] + elseif not queries.get_query(lang, 'textobjects') then + query = nil + end + if query then + api.nvim_buf_del_keymap(buf, "n", mapping) + end + end end return M diff --git a/lua/nvim-treesitter/ts_utils.lua b/lua/nvim-treesitter/ts_utils.lua index 0a5cbc608..eddd0f56b 100644 --- a/lua/nvim-treesitter/ts_utils.lua +++ b/lua/nvim-treesitter/ts_utils.lua @@ -13,7 +13,7 @@ function M.get_node_text(node, bufnr) if not node then return {} end -- We have to remember that end_col is end-exclusive - local start_row, start_col, end_row, end_col = node:range() + local start_row, start_col, end_row, end_col = M.get_node_range(node) if start_row ~= end_row then local lines = api.nvim_buf_get_lines(bufnr, start_row, end_row+1, false) @@ -131,12 +131,7 @@ end -- Set visual selection to node function M.update_selection(buf, node) - local start_row, start_col, end_row, end_col - if type(node) == 'table' then - start_row, start_col, end_row, end_col = unpack(node) - else - start_row, start_col, end_row, end_col = node:range() - end + local start_row, start_col, end_row, end_col = M.get_node_range(node) if end_row == vim.fn.line('$') then end_col = #vim.fn.getline('$') @@ -187,8 +182,16 @@ function M.is_in_node_range(node, line, col) end end +function M.get_node_range(node_or_range) + if type(node_or_range) == 'table' then + return unpack(node_or_range) + else + return node_or_range:range() + end +end + function M.node_to_lsp_range(node) - local start_line, start_col, end_line, end_col = node:range() + local start_line, start_col, end_line, end_col = M.get_node_range(node) local rtn = {} rtn.start = { line = start_line, character = start_col } rtn['end'] = { line = end_line, character = end_col } @@ -225,4 +228,52 @@ function M.memoize_by_buf_tick(fn, bufnr_fn) end end +function M.swap_nodes(node_or_range1, node_or_range2, bufnr, cursor_to_second) + if not node_or_range1 or not node_or_range2 then return end + local range1 = M.node_to_lsp_range(node_or_range1) + local range2 = M.node_to_lsp_range(node_or_range2) + + local text1 = M.get_node_text(node_or_range1) + local text2 = M.get_node_text(node_or_range2) + + local edit1 = { range = range1, newText = table.concat(text2, '\n') } + local edit2 = { range = range2, newText = table.concat(text1, '\n') } + vim.lsp.util.apply_text_edits({edit1, edit2}, bufnr) + + if cursor_to_second then + -- Set the item in jump list + vim.cmd "normal! m'" + + local char_delta = 0 + local line_delta = 0 + if range1["end"].line < range2.start.line + or (range1["end"].line == range2.start.line and range1["end"].character < range2.start.character) then + line_delta = #text2 - #text1 + end + + if range1["end"].line == range2.start.line and range1["end"].character < range2.start.character then + if line_delta ~= 0 then + --- why? + --correction_after_line_change = -range2.start.character + --text_now_before_range2 = #(text2[#text2]) + --space_between_ranges = range2.start.character - range1["end"].character + --char_delta = correction_after_line_change + text_now_before_range2 + space_between_ranges + --- Equivalent to: + char_delta = #(text2[#text2]) - range1["end"].character + + -- add range1.start.character if last line of range1 (now text2) does not start at 0 + if range1.start.line == range2.start.line + line_delta then + char_delta = char_delta + range1.start.character + end + else + char_delta = #(text2[#text2]) - #(text1[#text1]) + end + end + + api.nvim_win_set_cursor(api.nvim_get_current_win(), + {range2.start.line + 1 + line_delta, + range2.start.character + char_delta}) + end +end + return M -- cgit v1.2.3-70-g09d2 From 1642e37499e0d58952cb6b5e3bedc9126cc43e4a Mon Sep 17 00:00:00 2001 From: Stephan Seitz Date: Sun, 2 Aug 2020 18:47:01 +0200 Subject: Textobjects: Add goto_adjacent --- lua/nvim-treesitter/textobjects.lua | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) (limited to 'lua') diff --git a/lua/nvim-treesitter/textobjects.lua b/lua/nvim-treesitter/textobjects.lua index 9c312f44a..56241e7f3 100644 --- a/lua/nvim-treesitter/textobjects.lua +++ b/lua/nvim-treesitter/textobjects.lua @@ -101,7 +101,40 @@ function M.swap_textobject_previous(query_string) swap_textobject(query_string, -1) end +function M.goto_adjacent_textobejct(query_string, forward, start, same_parent) + local bufnr, _, node = get_textobject_at_point(query_string) + local ajacent_textobject + if forward then + ajacent_textobject = M.next_textobject(node, query_string, same_parent, bufnr) + else + ajacent_textobject = M.previous_textobject(node, query_string, same_parent, bufnr) + end + + if ajacent_textobject then + local adjacent_textobject_range = {ajacent_textobject:range()} + if start then + api.nvim_win_set_cursor(api.nvim_get_current_win(), { adjacent_textobject_range[1] + 1, adjacent_textobject_range[2] }) + else + api.nvim_win_set_cursor(api.nvim_get_current_win(), { adjacent_textobject_range[3] + 1, adjacent_textobject_range[4] }) + end + end +end + +M.goto_next_textobject_start = function(query_string) M.goto_adjacent_textobejct(query_string, 'forward', 'start', false) end +M.goto_next_textobject_end = function(query_string) M.goto_adjacent_textobejct(query_string, 'forward', false, false) end +M.goto_previous_textobject_start = function(query_string) M.goto_adjacent_textobejct(query_string, false, 'start', false) end +M.goto_previous_textobject_end = function(query_string) M.goto_adjacent_textobejct(query_string, false, false, false) end + +function M.goto_next_textobject_end(query_string) + local bufnr, _, node = get_textobject_at_point(query_string) + if not node then return end + local next_textobject = M.next_textobject(node, query_string, false, bufnr) + local next_textobject_range = next_textobject:range() + api.nvim_win_set_cursor(api.nvim_get_current_win(), { next_textobject_range[3] + 1, next_textobject_range[4] + 1 }) +end + function M.next_textobject(node, query_string, same_parent, bufnr) + local node = node or ts_utils.get_node_at_cursor() local bufnr = bufnr or api.nvim_get_current_buf() local matches = queries.get_capture_matches(bufnr, query_string, 'textobjects') @@ -129,6 +162,7 @@ function M.next_textobject(node, query_string, same_parent, bufnr) end function M.previous_textobject(node, query_string, same_parent, bufnr) + local node = node or ts_utils.get_node_at_cursor() local bufnr = bufnr or api.nvim_get_current_buf() local matches = queries.get_capture_matches(bufnr, query_string, 'textobjects') -- cgit v1.2.3-70-g09d2 From e629efafd8f529ff9b1297b947b4438bf4d2265c Mon Sep 17 00:00:00 2001 From: Stephan Seitz Date: Sun, 2 Aug 2020 19:35:52 +0200 Subject: Textobjects: provide mappings for all swap/goto functions --- README.md | 26 ++++++++- doc/nvim-treesitter.txt | 26 ++++++++- lua/nvim-treesitter/configs.lua | 8 ++- lua/nvim-treesitter/textobjects.lua | 110 ++++++++++++++++++++---------------- 4 files changed, 116 insertions(+), 54 deletions(-) (limited to 'lua') diff --git a/README.md b/README.md index 915fcf71a..21d6df24b 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,31 @@ require'nvim-treesitter.configs'.setup { ["ad"] = "@comment.outer", ["am"] = "@call.outer", ["im"] = "@call.inner" - } + }, + -- swap parameters (keymap -> textobject query) + swap_next = { + [""] = "@parameter.inner", + }, + swap_previous = { + [""] = "@parameter.inner", + }, + -- set mappings to go to start/end of adjacent textobjects (keymap -> textobject query) + goto_previous_start = { + ["[m"] = "@function.outer", + ["[["] = "@class.outer", + }, + goto_previous_end = { + ["[M"] = "@function.outer", + ["[]"] = "@class.outer", + }, + goto_next_start = { + ["]m"] = "@function.outer", + ["]]"] = "@class.outer", + }, + goto_next_end = { + ["]M"] = "@function.outer", + ["]["] = "@class.outer", + }, }, ensure_installed = "all" -- one of "all", "language", or a list of languages } diff --git a/doc/nvim-treesitter.txt b/doc/nvim-treesitter.txt index ed992e2d9..1f2d87569 100644 --- a/doc/nvim-treesitter.txt +++ b/doc/nvim-treesitter.txt @@ -94,7 +94,31 @@ By default, everything is disabled. To enable support for features, in your `ini ["ad"] = "@comment.outer", ["am"] = "@call.outer", ["im"] = "@call.inner" - } + }, + -- swap parameters (keymap -> textobject query) + swap_next = { + [""] = "@parameter.inner", + }, + swap_previous = { + [""] = "@parameter.inner", + }, + -- set mappings to go to start/end of adjacent textobjects (keymap -> textobject query) + goto_previous_start = { + ["[m"] = "@function.outer", + ["[["] = "@class.outer", + }, + goto_previous_end = { + ["[M"] = "@function.outer", + ["[]"] = "@class.outer", + }, + goto_next_start = { + ["]m"] = "@function.outer", + ["]]"] = "@class.outer", + }, + goto_next_end = { + ["]M"] = "@function.outer", + ["]["] = "@class.outer", + }, }, ensure_installed = "all" -- one of "all", "language", or a list of languages } diff --git a/lua/nvim-treesitter/configs.lua b/lua/nvim-treesitter/configs.lua index 350a8cdad..bcaa2819c 100644 --- a/lua/nvim-treesitter/configs.lua +++ b/lua/nvim-treesitter/configs.lua @@ -86,8 +86,12 @@ local builtin_modules = { return has_some_textobject_mapping(lang) or queries.has_textobjects(lang) end, keymaps = {}, - swap_next_keymaps = {}, - swap_previous_keymaps = {} + swap_next = {}, + swap_previous = {}, + goto_next_start = {}, + goto_next_end = {}, + goto_previous_start = {}, + goto_previous_end = {} } } diff --git a/lua/nvim-treesitter/textobjects.lua b/lua/nvim-treesitter/textobjects.lua index 56241e7f3..af2c1fff8 100644 --- a/lua/nvim-treesitter/textobjects.lua +++ b/lua/nvim-treesitter/textobjects.lua @@ -8,7 +8,7 @@ local ts_utils = require'nvim-treesitter.ts_utils' local M = {} -local function get_textobject_at_point(query_string) +local function textobject_at_point(query_string) local bufnr = vim.api.nvim_get_current_buf() local lang = parsers.get_buf_lang(bufnr) if not lang then return end @@ -74,59 +74,69 @@ local function get_textobject_at_point(query_string) end function M.select_textobject(query_string) - local bufnr, textobject = get_textobject_at_point(query_string) + local bufnr, textobject = textobject_at_point(query_string) if textobject then ts_utils.update_selection(bufnr, textobject) end end local function swap_textobject(query_string, direction) - local bufnr, textobject_range, node = get_textobject_at_point(query_string) + local bufnr, textobject_range, node = textobject_at_point(query_string) local step = direction > 0 and 1 or -1 if not node then return end for _ = 1, math.abs(direction), step do if direction > 0 then - ts_utils.swap_nodes(textobject_range, M.next_textobject(node, query_string, true, bufnr), bufnr, "yes, set cursor!") + ts_utils.swap_nodes(textobject_range, + M.next_textobject(node, query_string, true, bufnr), + bufnr, + "yes, set cursor!") else - ts_utils.swap_nodes(textobject_range, M.previous_textobject(node, query_string, true, bufnr), bufnr, "yes, set cursor!") + ts_utils.swap_nodes(textobject_range, + M.previous_textobject(node, query_string, true, bufnr), + bufnr, + "yes, set cursor!") end end end -function M.swap_textobject_next(query_string) +function M.swap_next(query_string) swap_textobject(query_string, 1) end -function M.swap_textobject_previous(query_string) +function M.swap_previous(query_string) swap_textobject(query_string, -1) end -function M.goto_adjacent_textobejct(query_string, forward, start, same_parent) - local bufnr, _, node = get_textobject_at_point(query_string) - local ajacent_textobject +function M.goto_adjacent(query_string, forward, start, same_parent) + local bufnr, _, node = textobject_at_point(query_string) + local adjacent_textobject if forward then - ajacent_textobject = M.next_textobject(node, query_string, same_parent, bufnr) + adjacent_textobject = M.next_textobject(node, query_string, same_parent, bufnr) else - ajacent_textobject = M.previous_textobject(node, query_string, same_parent, bufnr) + adjacent_textobject = M.previous_textobject(node, query_string, same_parent, bufnr) end - if ajacent_textobject then - local adjacent_textobject_range = {ajacent_textobject:range()} + if adjacent_textobject then + local adjacent_textobject_range = {adjacent_textobject:range()} if start then - api.nvim_win_set_cursor(api.nvim_get_current_win(), { adjacent_textobject_range[1] + 1, adjacent_textobject_range[2] }) + api.nvim_win_set_cursor(api.nvim_get_current_win(), + { adjacent_textobject_range[1] + 1, adjacent_textobject_range[2] }) else - api.nvim_win_set_cursor(api.nvim_get_current_win(), { adjacent_textobject_range[3] + 1, adjacent_textobject_range[4] }) + api.nvim_win_set_cursor(api.nvim_get_current_win(), + { adjacent_textobject_range[3] + 1, adjacent_textobject_range[4] }) end end end -M.goto_next_textobject_start = function(query_string) M.goto_adjacent_textobejct(query_string, 'forward', 'start', false) end -M.goto_next_textobject_end = function(query_string) M.goto_adjacent_textobejct(query_string, 'forward', false, false) end -M.goto_previous_textobject_start = function(query_string) M.goto_adjacent_textobejct(query_string, false, 'start', false) end -M.goto_previous_textobject_end = function(query_string) M.goto_adjacent_textobejct(query_string, false, false, false) end +-- luacheck: push ignore 631 +M.goto_next_start = function(query_string) M.goto_adjacent(query_string, 'forward', 'start', false) end +M.goto_next_end = function(query_string) M.goto_adjacent(query_string, 'forward', false, false) end +M.goto_previous_start = function(query_string) M.goto_adjacent(query_string, false, 'start', false) end +M.goto_previous_end = function(query_string) M.goto_adjacent(query_string, false, false, false) end +-- luacheck: pop -function M.goto_next_textobject_end(query_string) - local bufnr, _, node = get_textobject_at_point(query_string) +function M.goto_next_end(query_string) + local bufnr, _, node = textobject_at_point(query_string) if not node then return end local next_textobject = M.next_textobject(node, query_string, false, bufnr) local next_textobject_range = next_textobject:range() @@ -189,6 +199,13 @@ function M.previous_textobject(node, query_string, same_parent, bufnr) return previous_node and previous_node.node end +local normal_mode_functions = { "swap_next", + "swap_previous", + "goto_next_start", + "goto_next_end", + "goto_previous_start", + "goto_previous_end"} + function M.attach(bufnr, lang) local buf = bufnr or api.nvim_get_current_buf() local config = configs.get_module("textobjects") @@ -206,26 +223,17 @@ function M.attach(bufnr, lang) api.nvim_buf_set_keymap(buf, "v", mapping, cmd, {silent = true, noremap = true }) end end - for mapping, query in pairs(config.swap_next_keymaps) do - if type(query) == 'table' then - query = query[lang] - elseif not queries.get_query(lang, 'textobjects') then - query = nil - end - if query then - local cmd = ":lua require'nvim-treesitter.textobjects'.swap_textobject_next('"..query.."')" - api.nvim_buf_set_keymap(buf, "n", mapping, cmd, {silent = true}) - end - end - for mapping, query in pairs(config.swap_previous_keymaps) do - if type(query) == 'table' then - query = query[lang] - elseif not queries.get_query(lang, 'textobjects') then - query = nil - end - if query then - local cmd = ":lua require'nvim-treesitter.textobjects'.swap_textobject_previous('"..query.."')" - api.nvim_buf_set_keymap(buf, "n", mapping, cmd, {silent = true}) + for _, function_call in pairs(normal_mode_functions) do + for mapping, query in pairs(config[function_call] or {}) do + if type(query) == 'table' then + query = query[lang] + elseif not queries.get_query(lang, 'textobjects') then + query = nil + end + if query then + local cmd = ":lua require'nvim-treesitter.textobjects'."..function_call.."('"..query.."')" + api.nvim_buf_set_keymap(buf, "n", mapping, cmd, {silent = true, noremap = true }) + end end end end @@ -246,14 +254,16 @@ function M.detach(bufnr) api.nvim_buf_del_keymap(buf, "v", mapping) end end - for mapping, query in pairs(config.swap_next_keymaps) or pairs(config.swap_previous_keymaps) do - if type(query) == 'table' then - query = query[lang] - elseif not queries.get_query(lang, 'textobjects') then - query = nil - end - if query then - api.nvim_buf_del_keymap(buf, "n", mapping) + for _, function_call in pairs(normal_mode_functions) do + for mapping, query in pairs(config[function_call] or {}) do + if type(query) == 'table' then + query = query[lang] + elseif not queries.get_query(lang, 'textobjects') then + query = nil + end + if query then + api.nvim_buf_del_keymap(buf, "n", mapping) + end end end end -- cgit v1.2.3-70-g09d2 From f6681c230f0eb8be0f3f9375b2da205681ea1fc9 Mon Sep 17 00:00:00 2001 From: Stephan Seitz Date: Mon, 3 Aug 2020 00:25:12 +0200 Subject: chore(textobject): use query.find_best_match to find next/previous textobject --- lua/nvim-treesitter/query.lua | 30 ++++++++++++++++ lua/nvim-treesitter/textobjects.lua | 70 ++++++++++++++++--------------------- 2 files changed, 60 insertions(+), 40 deletions(-) (limited to 'lua') diff --git a/lua/nvim-treesitter/query.lua b/lua/nvim-treesitter/query.lua index c630b366c..034df223d 100644 --- a/lua/nvim-treesitter/query.lua +++ b/lua/nvim-treesitter/query.lua @@ -212,6 +212,36 @@ function M.get_capture_matches(bufnr, capture_string, query_group) return matches end +function M.find_best_match(bufnr, capture_string, query_group, filter_predicate, scoring_function) + if not string.sub(capture_string, 1,2) == '@' then + api.nvim_err_writeln('capture_string must start with "@"') + return + end + + --remove leading "@" + capture_string = string.sub(capture_string, 2) + + local best + local best_score + + for maybe_match in M.iter_group_results(bufnr, query_group) do + local match = utils.get_at_path(maybe_match, capture_string) + + if match and filter_predicate(match) then + local current_score = scoring_function(match) + if not best then + best = match + best_score = current_score + end + if current_score > best_score then + best = match + best_score = current_score + end + end + end + return best +end + -- Iterates matches from a query file. -- @param bufnr the buffer -- @param query_group the query file to use diff --git a/lua/nvim-treesitter/textobjects.lua b/lua/nvim-treesitter/textobjects.lua index af2c1fff8..0646a8ef0 100644 --- a/lua/nvim-treesitter/textobjects.lua +++ b/lua/nvim-treesitter/textobjects.lua @@ -145,56 +145,46 @@ end function M.next_textobject(node, query_string, same_parent, bufnr) local node = node or ts_utils.get_node_at_cursor() + if not node then return end + local _, _, node_end = node:end_() local bufnr = bufnr or api.nvim_get_current_buf() - local matches = queries.get_capture_matches(bufnr, query_string, 'textobjects') - local _, _ , node_end = node:end_() - local next_node - local next_node_start - - for _, m in pairs(matches) do - local _, _, other_end = m.node:start() - if other_end > node_end then - if not same_parent or node:parent() == m.node:parent() then - if not next_node then - next_node = m - _, _, next_node_start = next_node.node:start() - end - if other_end < next_node_start then - next_node = m - _, _, next_node_start = next_node.node:start() - end - end - end - end + local next_node = queries.find_best_match(bufnr, + query_string, + 'textobjects', + function(match) + if not same_parent or node:parent() == match.node:parent() then + local _, _, start = match.node:start() + return start > node_end + end + end, + function(match) + local _, _, node_start = match.node:start() + return -node_start + end) return next_node and next_node.node end function M.previous_textobject(node, query_string, same_parent, bufnr) local node = node or ts_utils.get_node_at_cursor() + if not node then return end + local _, _, node_start = node:start() local bufnr = bufnr or api.nvim_get_current_buf() - local matches = queries.get_capture_matches(bufnr, query_string, 'textobjects') - local _, _ , node_start = node:start() - local previous_node - local previous_node_end - - for _, m in pairs(matches) do - local _, _, other_end = m.node:end_() - if other_end < node_start then - if not same_parent or node:parent() == m.node:parent() then - if not previous_node then - previous_node = m - _, _, previous_node_end = previous_node.node:end_() - end - if other_end > previous_node_end then - previous_node = m - _, _, previous_node_end = previous_node.node:end_() - end - end - end - end + local previous_node = queries.find_best_match(bufnr, + query_string, + 'textobjects', + function(match) + if not same_parent or node:parent() == match.node:parent() then + local _, _, end_ = match.node:end_() + return end_ < node_start + end + end, + function(match) + local _, _, node_end = match.node:end_() + return node_end + end) return previous_node and previous_node.node end -- cgit v1.2.3-70-g09d2 From be2cfc1bca7194de4eb9dcdf9d3867be12c8b767 Mon Sep 17 00:00:00 2001 From: Stephan Seitz Date: Tue, 4 Aug 2020 19:11:59 +0200 Subject: Textobjects: Allow nested textobjects on goto_adjacent --- lua/nvim-treesitter/textobjects.lua | 60 ++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 24 deletions(-) (limited to 'lua') diff --git a/lua/nvim-treesitter/textobjects.lua b/lua/nvim-treesitter/textobjects.lua index 0646a8ef0..4ea9c4bb2 100644 --- a/lua/nvim-treesitter/textobjects.lua +++ b/lua/nvim-treesitter/textobjects.lua @@ -84,15 +84,17 @@ local function swap_textobject(query_string, direction) local bufnr, textobject_range, node = textobject_at_point(query_string) local step = direction > 0 and 1 or -1 if not node then return end + local overlapping_range_ok = false + local same_parent = true for _ = 1, math.abs(direction), step do if direction > 0 then ts_utils.swap_nodes(textobject_range, - M.next_textobject(node, query_string, true, bufnr), + M.next_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr), bufnr, "yes, set cursor!") else ts_utils.swap_nodes(textobject_range, - M.previous_textobject(node, query_string, true, bufnr), + M.previous_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr), bufnr, "yes, set cursor!") end @@ -107,13 +109,13 @@ function M.swap_previous(query_string) swap_textobject(query_string, -1) end -function M.goto_adjacent(query_string, forward, start, same_parent) +function M.goto_adjacent(query_string, forward, start, same_parent, overlapping_range_ok) local bufnr, _, node = textobject_at_point(query_string) local adjacent_textobject if forward then - adjacent_textobject = M.next_textobject(node, query_string, same_parent, bufnr) + adjacent_textobject = M.next_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr) else - adjacent_textobject = M.previous_textobject(node, query_string, same_parent, bufnr) + adjacent_textobject = M.previous_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr) end if adjacent_textobject then @@ -129,33 +131,35 @@ function M.goto_adjacent(query_string, forward, start, same_parent) end -- luacheck: push ignore 631 -M.goto_next_start = function(query_string) M.goto_adjacent(query_string, 'forward', 'start', false) end -M.goto_next_end = function(query_string) M.goto_adjacent(query_string, 'forward', false, false) end -M.goto_previous_start = function(query_string) M.goto_adjacent(query_string, false, 'start', false) end -M.goto_previous_end = function(query_string) M.goto_adjacent(query_string, false, false, false) end +M.goto_next_start = function(query_string) M.goto_adjacent(query_string, 'forward', 'start', not 'same_parent', 'overlap ok') end +M.goto_next_end = function(query_string) M.goto_adjacent(query_string, 'forward', not 'start', not 'same_parent', 'overlap ok') end +M.goto_previous_start = function(query_string) M.goto_adjacent(query_string, not 'forward', 'start', not 'same_parent', 'overlap ok') end +M.goto_previous_end = function(query_string) M.goto_adjacent(query_string, not 'forward', not 'start', not 'same_parent', 'overlap ok') end -- luacheck: pop -function M.goto_next_end(query_string) - local bufnr, _, node = textobject_at_point(query_string) - if not node then return end - local next_textobject = M.next_textobject(node, query_string, false, bufnr) - local next_textobject_range = next_textobject:range() - api.nvim_win_set_cursor(api.nvim_get_current_win(), { next_textobject_range[3] + 1, next_textobject_range[4] + 1 }) -end - -function M.next_textobject(node, query_string, same_parent, bufnr) +function M.next_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr) local node = node or ts_utils.get_node_at_cursor() + local bufnr = bufnr or api.nvim_get_current_buf() if not node then return end + local _, _, node_end = node:end_() - local bufnr = bufnr or api.nvim_get_current_buf() + local search_start, _ + if overlapping_range_ok then + _, _, search_start = node:start() + search_start = search_start - 1 + else + _, _, search_start = node:end_() + end local next_node = queries.find_best_match(bufnr, query_string, 'textobjects', function(match) + if match.node == node then return end if not same_parent or node:parent() == match.node:parent() then local _, _, start = match.node:start() - return start > node_end + local _, _, end_ = match.node:end_() + return start > search_start and end_ > node_end end end, function(match) @@ -166,19 +170,27 @@ function M.next_textobject(node, query_string, same_parent, bufnr) return next_node and next_node.node end -function M.previous_textobject(node, query_string, same_parent, bufnr) +function M.previous_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr) local node = node or ts_utils.get_node_at_cursor() - if not node then return end - local _, _, node_start = node:start() local bufnr = bufnr or api.nvim_get_current_buf() + if not node then return end + local _, _, node_start = node:start() + local search_end, _ + if overlapping_range_ok then + _, _, search_end = node:end_() + search_end = search_end + 1 + else + _, _, search_end = node:start() + end local previous_node = queries.find_best_match(bufnr, query_string, 'textobjects', function(match) if not same_parent or node:parent() == match.node:parent() then local _, _, end_ = match.node:end_() - return end_ < node_start + local _, _, start = match.node:start() + return end_ < search_end and start < node_start end end, function(match) -- cgit v1.2.3-70-g09d2 From f3ed370643dee45a3ea253a780cd30e74361d2bd Mon Sep 17 00:00:00 2001 From: Stephan Seitz Date: Wed, 5 Aug 2020 18:11:01 +0200 Subject: Textobject goto: treat end differently that start --- lua/nvim-treesitter/textobjects.lua | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) (limited to 'lua') diff --git a/lua/nvim-treesitter/textobjects.lua b/lua/nvim-treesitter/textobjects.lua index 4ea9c4bb2..1220acedd 100644 --- a/lua/nvim-treesitter/textobjects.lua +++ b/lua/nvim-treesitter/textobjects.lua @@ -87,17 +87,17 @@ local function swap_textobject(query_string, direction) local overlapping_range_ok = false local same_parent = true for _ = 1, math.abs(direction), step do - if direction > 0 then - ts_utils.swap_nodes(textobject_range, - M.next_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr), - bufnr, - "yes, set cursor!") - else - ts_utils.swap_nodes(textobject_range, - M.previous_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr), - bufnr, - "yes, set cursor!") - end + if direction > 0 then + ts_utils.swap_nodes(textobject_range, + M.next_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr), + bufnr, + "yes, set cursor!") + else + ts_utils.swap_nodes(textobject_range, + M.previous_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr), + bufnr, + "yes, set cursor!") + end end end @@ -146,7 +146,6 @@ function M.next_textobject(node, query_string, same_parent, overlapping_range_ok local search_start, _ if overlapping_range_ok then _, _, search_start = node:start() - search_start = search_start - 1 else _, _, search_start = node:end_() end @@ -159,7 +158,7 @@ function M.next_textobject(node, query_string, same_parent, overlapping_range_ok if not same_parent or node:parent() == match.node:parent() then local _, _, start = match.node:start() local _, _, end_ = match.node:end_() - return start > search_start and end_ > node_end + return start > search_start and end_ >= node_end end end, function(match) -- cgit v1.2.3-70-g09d2 From 32271b26ef64433b2d9cc41392594db614449a4c Mon Sep 17 00:00:00 2001 From: Stephan Seitz Date: Thu, 6 Aug 2020 09:01:38 +0200 Subject: Textobjects: set jump before going to adjacent_textobject --- lua/nvim-treesitter/refactor/navigation.lua | 4 +- lua/nvim-treesitter/textobjects.lua | 164 ++++++++++++++-------------- lua/nvim-treesitter/ts_utils.lua | 4 +- lua/nvim-treesitter/utils.lua | 4 + 4 files changed, 88 insertions(+), 88 deletions(-) (limited to 'lua') diff --git a/lua/nvim-treesitter/refactor/navigation.lua b/lua/nvim-treesitter/refactor/navigation.lua index a3fee20e8..5f5eebd53 100644 --- a/lua/nvim-treesitter/refactor/navigation.lua +++ b/lua/nvim-treesitter/refactor/navigation.lua @@ -1,6 +1,7 @@ -- Definition based navigation module local ts_utils = require'nvim-treesitter.ts_utils' +local utils = require'nvim-treesitter.utils' local locals = require'nvim-treesitter.locals' local configs = require'nvim-treesitter.configs' local api = vim.api @@ -11,8 +12,7 @@ function M.goto_definition(bufnr) local bufnr = bufnr or api.nvim_get_current_buf() local node_at_point = ts_utils.get_node_at_cursor() - -- Set the item in jump list - vim.cmd "normal! m'" + utils.set_jump() if not node_at_point then return end diff --git a/lua/nvim-treesitter/textobjects.lua b/lua/nvim-treesitter/textobjects.lua index 1220acedd..95b46cabc 100644 --- a/lua/nvim-treesitter/textobjects.lua +++ b/lua/nvim-treesitter/textobjects.lua @@ -5,6 +5,7 @@ local configs = require "nvim-treesitter.configs" local parsers = require "nvim-treesitter.parsers" local queries = require'nvim-treesitter.query' local ts_utils = require'nvim-treesitter.ts_utils' +local utils = require'nvim-treesitter.utils' local M = {} @@ -80,63 +81,11 @@ function M.select_textobject(query_string) end end -local function swap_textobject(query_string, direction) - local bufnr, textobject_range, node = textobject_at_point(query_string) - local step = direction > 0 and 1 or -1 - if not node then return end - local overlapping_range_ok = false - local same_parent = true - for _ = 1, math.abs(direction), step do - if direction > 0 then - ts_utils.swap_nodes(textobject_range, - M.next_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr), - bufnr, - "yes, set cursor!") - else - ts_utils.swap_nodes(textobject_range, - M.previous_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr), - bufnr, - "yes, set cursor!") - end - end -end - -function M.swap_next(query_string) - swap_textobject(query_string, 1) +local function get_adjacent(forward, node, query_string, same_parent, overlapping_range_ok, bufnr) + local fn = forward and M.next_textobject or M.previous_textobject + return fn(node, query_string, same_parent, overlapping_range_ok, bufnr) end -function M.swap_previous(query_string) - swap_textobject(query_string, -1) -end - -function M.goto_adjacent(query_string, forward, start, same_parent, overlapping_range_ok) - local bufnr, _, node = textobject_at_point(query_string) - local adjacent_textobject - if forward then - adjacent_textobject = M.next_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr) - else - adjacent_textobject = M.previous_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr) - end - - if adjacent_textobject then - local adjacent_textobject_range = {adjacent_textobject:range()} - if start then - api.nvim_win_set_cursor(api.nvim_get_current_win(), - { adjacent_textobject_range[1] + 1, adjacent_textobject_range[2] }) - else - api.nvim_win_set_cursor(api.nvim_get_current_win(), - { adjacent_textobject_range[3] + 1, adjacent_textobject_range[4] }) - end - end -end - --- luacheck: push ignore 631 -M.goto_next_start = function(query_string) M.goto_adjacent(query_string, 'forward', 'start', not 'same_parent', 'overlap ok') end -M.goto_next_end = function(query_string) M.goto_adjacent(query_string, 'forward', not 'start', not 'same_parent', 'overlap ok') end -M.goto_previous_start = function(query_string) M.goto_adjacent(query_string, not 'forward', 'start', not 'same_parent', 'overlap ok') end -M.goto_previous_end = function(query_string) M.goto_adjacent(query_string, not 'forward', not 'start', not 'same_parent', 'overlap ok') end --- luacheck: pop - function M.next_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr) local node = node or ts_utils.get_node_at_cursor() local bufnr = bufnr or api.nvim_get_current_buf() @@ -149,22 +98,20 @@ function M.next_textobject(node, query_string, same_parent, overlapping_range_ok else _, _, search_start = node:end_() end + local function scoring_function(match) + if match.node == node then return end + if not same_parent or node:parent() == match.node:parent() then + local _, _, start = match.node:start() + local _, _, end_ = match.node:end_() + return start > search_start and end_ >= node_end + end + end + local function filter_function(match) + local _, _, node_start = match.node:start() + return -node_start + end - local next_node = queries.find_best_match(bufnr, - query_string, - 'textobjects', - function(match) - if match.node == node then return end - if not same_parent or node:parent() == match.node:parent() then - local _, _, start = match.node:start() - local _, _, end_ = match.node:end_() - return start > search_start and end_ >= node_end - end - end, - function(match) - local _, _, node_start = match.node:start() - return -node_start - end) + local next_node = queries.find_best_match(bufnr, query_string, 'textobjects', scoring_function, filter_function) return next_node and next_node.node end @@ -182,24 +129,73 @@ function M.previous_textobject(node, query_string, same_parent, overlapping_rang else _, _, search_end = node:start() end - local previous_node = queries.find_best_match(bufnr, - query_string, - 'textobjects', - function(match) - if not same_parent or node:parent() == match.node:parent() then - local _, _, end_ = match.node:end_() - local _, _, start = match.node:start() - return end_ < search_end and start < node_start - end - end, - function(match) - local _, _, node_end = match.node:end_() - return node_end - end) + + local function scoring_function(match) + if not same_parent or node:parent() == match.node:parent() then + local _, _, end_ = match.node:end_() + local _, _, start = match.node:start() + return end_ < search_end and start < node_start + end + end + + local function filter_function(match) + local _, _, node_end = match.node:end_() + return node_end + end + + local previous_node = queries.find_best_match(bufnr, query_string, 'textobjects', scoring_function, filter_function) return previous_node and previous_node.node end +function M.goto_adjacent(query_string, forward, start, same_parent, overlapping_range_ok) + local bufnr, _, node = textobject_at_point(query_string) + local adjacent_textobject = get_adjacent(forward, node, query_string, same_parent, overlapping_range_ok, bufnr) + + if adjacent_textobject then + utils.set_jump() + + local adjacent_textobject_range = {adjacent_textobject:range()} + local position + if start then + position = { adjacent_textobject_range[1] + 1, adjacent_textobject_range[2] } + else + position = { adjacent_textobject_range[3] + 1, adjacent_textobject_range[4] } + end + api.nvim_win_set_cursor(api.nvim_get_current_win(), position) + end +end + +local function swap_textobject(query_string, direction) + local bufnr, textobject_range, node = textobject_at_point(query_string) + if not node then return end + + local step = direction > 0 and 1 or -1 + local overlapping_range_ok = false + local same_parent = true + for _ = 1, math.abs(direction), step do + local forward = direction > 0 + local adjacent_textobject = get_adjacent(forward, node, query_string, same_parent, overlapping_range_ok, bufnr) + ts_utils.swap_nodes(textobject_range, adjacent_textobject, bufnr, "yes, set cursor!") + end +end + +function M.swap_next(query_string) + swap_textobject(query_string, 1) +end + +function M.swap_previous(query_string) + swap_textobject(query_string, -1) +end + + +-- luacheck: push ignore 631 +M.goto_next_start = function(query_string) M.goto_adjacent(query_string, 'forward', 'start', not 'same_parent', 'overlap ok') end +M.goto_next_end = function(query_string) M.goto_adjacent(query_string, 'forward', not 'start', not 'same_parent', 'overlap ok') end +M.goto_previous_start = function(query_string) M.goto_adjacent(query_string, not 'forward', 'start', not 'same_parent', 'overlap ok') end +M.goto_previous_end = function(query_string) M.goto_adjacent(query_string, not 'forward', not 'start', not 'same_parent', 'overlap ok') end +-- luacheck: pop + local normal_mode_functions = { "swap_next", "swap_previous", "goto_next_start", diff --git a/lua/nvim-treesitter/ts_utils.lua b/lua/nvim-treesitter/ts_utils.lua index eddd0f56b..52fdd0fc8 100644 --- a/lua/nvim-treesitter/ts_utils.lua +++ b/lua/nvim-treesitter/ts_utils.lua @@ -1,6 +1,7 @@ local api = vim.api local parsers = require'nvim-treesitter.parsers' +local utils = require'nvim-treesitter.utils' local M = {} @@ -241,8 +242,7 @@ function M.swap_nodes(node_or_range1, node_or_range2, bufnr, cursor_to_second) vim.lsp.util.apply_text_edits({edit1, edit2}, bufnr) if cursor_to_second then - -- Set the item in jump list - vim.cmd "normal! m'" + utils.set_jump() local char_delta = 0 local line_delta = 0 diff --git a/lua/nvim-treesitter/utils.lua b/lua/nvim-treesitter/utils.lua index 416326e48..d3d183395 100644 --- a/lua/nvim-treesitter/utils.lua +++ b/lua/nvim-treesitter/utils.lua @@ -67,4 +67,8 @@ function M.print_warning(text) api.nvim_command(string.format([[echohl WarningMsg | echo "%s" | echohl None]], text)) end +function M.set_jump() + vim.cmd "normal! m'" +end + return M -- cgit v1.2.3-70-g09d2 From 52168114594d791a3ae6092ab2489758da7b3ae8 Mon Sep 17 00:00:00 2001 From: Stephan Seitz Date: Sun, 16 Aug 2020 14:50:54 +0200 Subject: chore(textobjects): split up into submodules --- lua/nvim-treesitter/configs.lua | 42 +++-- lua/nvim-treesitter/textobjects.lua | 268 ----------------------------- lua/nvim-treesitter/textobjects/attach.lua | 61 +++++++ lua/nvim-treesitter/textobjects/move.lua | 41 +++++ lua/nvim-treesitter/textobjects/select.lua | 55 ++++++ lua/nvim-treesitter/textobjects/shared.lua | 142 +++++++++++++++ lua/nvim-treesitter/textobjects/swap.lua | 35 ++++ 7 files changed, 362 insertions(+), 282 deletions(-) delete mode 100644 lua/nvim-treesitter/textobjects.lua create mode 100644 lua/nvim-treesitter/textobjects/attach.lua create mode 100644 lua/nvim-treesitter/textobjects/move.lua create mode 100644 lua/nvim-treesitter/textobjects/select.lua create mode 100644 lua/nvim-treesitter/textobjects/shared.lua create mode 100644 lua/nvim-treesitter/textobjects/swap.lua (limited to 'lua') diff --git a/lua/nvim-treesitter/configs.lua b/lua/nvim-treesitter/configs.lua index bcaa2819c..ba4ae3f8d 100644 --- a/lua/nvim-treesitter/configs.lua +++ b/lua/nvim-treesitter/configs.lua @@ -7,7 +7,7 @@ local utils = require'nvim-treesitter.utils' local M = {} local function has_some_textobject_mapping(lang) - for _, v in pairs(M.get_module('textobjects').keymaps) do + for _, v in pairs(M.get_module('textobjects.select').keymaps) do if type(v) == 'table' then if v[lang] then return true @@ -79,19 +79,33 @@ local builtin_modules = { } }, textobjects = { - module_path = 'nvim-treesitter.textobjects', - enable = false, - disable = {}, - is_supported = function(lang) - return has_some_textobject_mapping(lang) or queries.has_textobjects(lang) - end, - keymaps = {}, - swap_next = {}, - swap_previous = {}, - goto_next_start = {}, - goto_next_end = {}, - goto_previous_start = {}, - goto_previous_end = {} + select = { + module_path = 'nvim-treesitter.textobjects.select', + enable = false, + disable = {}, + is_supported = function(lang) + return has_some_textobject_mapping(lang) or queries.has_textobjects(lang) + end, + keymaps = {}, + }, + move = { + module_path = 'nvim-treesitter.textobjects.move', + enable = false, + disable = {}, + is_supported = queries.has_textobjects, + goto_next_start = {}, + goto_next_end = {}, + goto_previous_start = {}, + goto_previous_end = {}, + }, + swap = { + module_path = 'nvim-treesitter.textobjects.swap', + enable = false, + disable = {}, + is_supported = queries.has_textobjects, + swap_next = {}, + swap_previous = {}, + }, } } diff --git a/lua/nvim-treesitter/textobjects.lua b/lua/nvim-treesitter/textobjects.lua deleted file mode 100644 index 95b46cabc..000000000 --- a/lua/nvim-treesitter/textobjects.lua +++ /dev/null @@ -1,268 +0,0 @@ -local api = vim.api -local ts = vim.treesitter - -local configs = require "nvim-treesitter.configs" -local parsers = require "nvim-treesitter.parsers" -local queries = require'nvim-treesitter.query' -local ts_utils = require'nvim-treesitter.ts_utils' -local utils = require'nvim-treesitter.utils' - -local M = {} - -local function textobject_at_point(query_string) - local bufnr = vim.api.nvim_get_current_buf() - local lang = parsers.get_buf_lang(bufnr) - if not lang then return end - - local row, col = unpack(vim.api.nvim_win_get_cursor(0)) - row = row - 1 - - local matches = {} - - if string.match(query_string, '^@.*') then - matches = queries.get_capture_matches(bufnr, query_string, 'textobjects') - else - local parser = parsers.get_parser(bufnr, lang) - local root = parser:parse():root() - - local start_row, _, end_row, _ = root:range() - - local query = ts.parse_query(lang, query_string) - for m in queries.iter_prepared_matches(query, root, bufnr, start_row, end_row) do - for _, n in pairs(m) do - if n.node then - table.insert(matches, n) - end - end - end - end - - local match_length - local smallest_range - local earliest_start - - for _, m in pairs(matches) do - if m.node and ts_utils.is_in_node_range(m.node, row, col) then - local length = ts_utils.node_length(m.node) - if not match_length or length < match_length then - smallest_range = m - match_length = length - end - -- for nodes with same length take the one with earliest start - if match_length and length == smallest_range then - local start = m.start - if start then - local _, _, start_byte = m.start.node:start() - if not earliest_start or start_byte < earliest_start then - smallest_range = m - match_length = length - earliest_start = start_byte - end - end - end - end - end - - if smallest_range then - if smallest_range.start then - local start_range = {smallest_range.start.node:range()} - local node_range = {smallest_range.node:range()} - return bufnr, {start_range[1], start_range[2], node_range[3], node_range[4]}, smallest_range.node - else - return bufnr, {smallest_range.node:range()}, smallest_range.node - end - end -end - -function M.select_textobject(query_string) - local bufnr, textobject = textobject_at_point(query_string) - if textobject then - ts_utils.update_selection(bufnr, textobject) - end -end - -local function get_adjacent(forward, node, query_string, same_parent, overlapping_range_ok, bufnr) - local fn = forward and M.next_textobject or M.previous_textobject - return fn(node, query_string, same_parent, overlapping_range_ok, bufnr) -end - -function M.next_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr) - local node = node or ts_utils.get_node_at_cursor() - local bufnr = bufnr or api.nvim_get_current_buf() - if not node then return end - - local _, _, node_end = node:end_() - local search_start, _ - if overlapping_range_ok then - _, _, search_start = node:start() - else - _, _, search_start = node:end_() - end - local function scoring_function(match) - if match.node == node then return end - if not same_parent or node:parent() == match.node:parent() then - local _, _, start = match.node:start() - local _, _, end_ = match.node:end_() - return start > search_start and end_ >= node_end - end - end - local function filter_function(match) - local _, _, node_start = match.node:start() - return -node_start - end - - local next_node = queries.find_best_match(bufnr, query_string, 'textobjects', scoring_function, filter_function) - - return next_node and next_node.node -end - -function M.previous_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr) - local node = node or ts_utils.get_node_at_cursor() - local bufnr = bufnr or api.nvim_get_current_buf() - if not node then return end - - local _, _, node_start = node:start() - local search_end, _ - if overlapping_range_ok then - _, _, search_end = node:end_() - search_end = search_end + 1 - else - _, _, search_end = node:start() - end - - local function scoring_function(match) - if not same_parent or node:parent() == match.node:parent() then - local _, _, end_ = match.node:end_() - local _, _, start = match.node:start() - return end_ < search_end and start < node_start - end - end - - local function filter_function(match) - local _, _, node_end = match.node:end_() - return node_end - end - - local previous_node = queries.find_best_match(bufnr, query_string, 'textobjects', scoring_function, filter_function) - - return previous_node and previous_node.node -end - -function M.goto_adjacent(query_string, forward, start, same_parent, overlapping_range_ok) - local bufnr, _, node = textobject_at_point(query_string) - local adjacent_textobject = get_adjacent(forward, node, query_string, same_parent, overlapping_range_ok, bufnr) - - if adjacent_textobject then - utils.set_jump() - - local adjacent_textobject_range = {adjacent_textobject:range()} - local position - if start then - position = { adjacent_textobject_range[1] + 1, adjacent_textobject_range[2] } - else - position = { adjacent_textobject_range[3] + 1, adjacent_textobject_range[4] } - end - api.nvim_win_set_cursor(api.nvim_get_current_win(), position) - end -end - -local function swap_textobject(query_string, direction) - local bufnr, textobject_range, node = textobject_at_point(query_string) - if not node then return end - - local step = direction > 0 and 1 or -1 - local overlapping_range_ok = false - local same_parent = true - for _ = 1, math.abs(direction), step do - local forward = direction > 0 - local adjacent_textobject = get_adjacent(forward, node, query_string, same_parent, overlapping_range_ok, bufnr) - ts_utils.swap_nodes(textobject_range, adjacent_textobject, bufnr, "yes, set cursor!") - end -end - -function M.swap_next(query_string) - swap_textobject(query_string, 1) -end - -function M.swap_previous(query_string) - swap_textobject(query_string, -1) -end - - --- luacheck: push ignore 631 -M.goto_next_start = function(query_string) M.goto_adjacent(query_string, 'forward', 'start', not 'same_parent', 'overlap ok') end -M.goto_next_end = function(query_string) M.goto_adjacent(query_string, 'forward', not 'start', not 'same_parent', 'overlap ok') end -M.goto_previous_start = function(query_string) M.goto_adjacent(query_string, not 'forward', 'start', not 'same_parent', 'overlap ok') end -M.goto_previous_end = function(query_string) M.goto_adjacent(query_string, not 'forward', not 'start', not 'same_parent', 'overlap ok') end --- luacheck: pop - -local normal_mode_functions = { "swap_next", - "swap_previous", - "goto_next_start", - "goto_next_end", - "goto_previous_start", - "goto_previous_end"} - -function M.attach(bufnr, lang) - local buf = bufnr or api.nvim_get_current_buf() - local config = configs.get_module("textobjects") - local lang = lang or parsers.get_buf_lang(buf) - - for mapping, query in pairs(config.keymaps) do - if type(query) == 'table' then - query = query[lang] - elseif not queries.get_query(lang, 'textobjects') then - query = nil - end - if query then - local cmd = ":lua require'nvim-treesitter.textobjects'.select_textobject('"..query.."')" - api.nvim_buf_set_keymap(buf, "o", mapping, cmd, {silent = true, noremap = true }) - api.nvim_buf_set_keymap(buf, "v", mapping, cmd, {silent = true, noremap = true }) - end - end - for _, function_call in pairs(normal_mode_functions) do - for mapping, query in pairs(config[function_call] or {}) do - if type(query) == 'table' then - query = query[lang] - elseif not queries.get_query(lang, 'textobjects') then - query = nil - end - if query then - local cmd = ":lua require'nvim-treesitter.textobjects'."..function_call.."('"..query.."')" - api.nvim_buf_set_keymap(buf, "n", mapping, cmd, {silent = true, noremap = true }) - end - end - end -end - -function M.detach(bufnr) - local buf = bufnr or api.nvim_get_current_buf() - local config = configs.get_module("textobjects") - local lang = parsers.get_buf_lang(bufnr) - - for mapping, query in pairs(config.keymaps) do - if type(query) == 'table' then - query = query[lang] - elseif not queries.get_query(lang, 'textobjects') then - query = nil - end - if query then - api.nvim_buf_del_keymap(buf, "o", mapping) - api.nvim_buf_del_keymap(buf, "v", mapping) - end - end - for _, function_call in pairs(normal_mode_functions) do - for mapping, query in pairs(config[function_call] or {}) do - if type(query) == 'table' then - query = query[lang] - elseif not queries.get_query(lang, 'textobjects') then - query = nil - end - if query then - api.nvim_buf_del_keymap(buf, "n", mapping) - end - end - end -end - -return M diff --git a/lua/nvim-treesitter/textobjects/attach.lua b/lua/nvim-treesitter/textobjects/attach.lua new file mode 100644 index 000000000..22d6b650d --- /dev/null +++ b/lua/nvim-treesitter/textobjects/attach.lua @@ -0,0 +1,61 @@ +local configs = require'nvim-treesitter.configs' +local parsers = require'nvim-treesitter.parsers' +local queries = require'nvim-treesitter.query' +local api = vim.api +local M = {} + +function M.make_attach(normal_mode_functions, submodule) + return function(bufnr, lang) + local buf = bufnr or api.nvim_get_current_buf() + local config = configs.get_module("textobjects."..submodule) + local lang = lang or parsers.get_buf_lang(buf) + + for _, function_call in pairs(normal_mode_functions) do + for mapping, query in pairs(config[function_call] or {}) do + if type(query) == 'table' then + query = query[lang] + elseif not queries.get_query(lang, 'textobjects') then + query = nil + end + if query then + local cmd = ":lua require'nvim-treesitter.textobjects."..submodule.."'."..function_call.."('"..query.."')" + api.nvim_buf_set_keymap(buf, "n", mapping, cmd, {silent = true, noremap = true }) + end + end + end + end +end + +function M.make_detach(normal_mode_functions, submodule) + return function(bufnr) + local buf = bufnr or api.nvim_get_current_buf() + local config = configs.get_module("textobjects."..submodule) + local lang = parsers.get_buf_lang(bufnr) + + for mapping, query in pairs(config.keymaps) do + if type(query) == 'table' then + query = query[lang] + elseif not queries.get_query(lang, 'textobjects') then + query = nil + end + if query then + api.nvim_buf_del_keymap(buf, "o", mapping) + api.nvim_buf_del_keymap(buf, "v", mapping) + end + end + for _, function_call in pairs(normal_mode_functions) do + for mapping, query in pairs(config[function_call] or {}) do + if type(query) == 'table' then + query = query[lang] + elseif not queries.get_query(lang, 'textobjects') then + query = nil + end + if query then + api.nvim_buf_del_keymap(buf, "n", mapping) + end + end + end + end +end + +return M diff --git a/lua/nvim-treesitter/textobjects/move.lua b/lua/nvim-treesitter/textobjects/move.lua new file mode 100644 index 000000000..8cfb71188 --- /dev/null +++ b/lua/nvim-treesitter/textobjects/move.lua @@ -0,0 +1,41 @@ +local utils = require'nvim-treesitter.utils' +local shared = require'nvim-treesitter.textobjects.shared' +local attach = require'nvim-treesitter.textobjects.attach' +local api = vim.api + +local M = {} + +function M.goto_adjacent(query_string, forward, start, same_parent, overlapping_range_ok) + local bufnr, _, node = shared.textobject_at_point(query_string) + local adjacent = shared.get_adjacent(forward, node, query_string, same_parent, overlapping_range_ok, bufnr) + + if adjacent then + utils.set_jump() + + local adjacent_textobject_range = {adjacent:range()} + local position + if start then + position = { adjacent_textobject_range[1] + 1, adjacent_textobject_range[2] } + else + position = { adjacent_textobject_range[3] + 1, adjacent_textobject_range[4] } + end + api.nvim_win_set_cursor(api.nvim_get_current_win(), position) + end +end + +-- luacheck: push ignore 631 +M.goto_next_start = function(query_string) M.goto_adjacent(query_string, 'forward', 'start', not 'same_parent', 'overlap ok') end +M.goto_next_end = function(query_string) M.goto_adjacent(query_string, 'forward', not 'start', not 'same_parent', 'overlap ok') end +M.goto_previous_start = function(query_string) M.goto_adjacent(query_string, not 'forward', 'start', not 'same_parent', 'overlap ok') end +M.goto_previous_end = function(query_string) M.goto_adjacent(query_string, not 'forward', not 'start', not 'same_parent', 'overlap ok') end +-- luacheck: pop + +local normal_mode_functions = {"goto_next_start", + "goto_next_end", + "goto_previous_start", + "goto_previous_end"} + +M.attach = attach.make_attach(normal_mode_functions, "move") +M.deattach = attach.make_detach(normal_mode_functions, "move") + +return M diff --git a/lua/nvim-treesitter/textobjects/select.lua b/lua/nvim-treesitter/textobjects/select.lua new file mode 100644 index 000000000..1f3698bc4 --- /dev/null +++ b/lua/nvim-treesitter/textobjects/select.lua @@ -0,0 +1,55 @@ +local api = vim.api +local configs = require'nvim-treesitter.configs' +local parsers = require'nvim-treesitter.parsers' +local queries = require'nvim-treesitter.query' + +local shared = require'nvim-treesitter.textobjects.shared' +local ts_utils = require'nvim-treesitter.ts_utils' + +local M = {} + +function M.select_textobject(query_string) + local bufnr, textobject = shared.textobject_at_point(query_string) + if textobject then + ts_utils.update_selection(bufnr, textobject) + end +end + +function M.attach(bufnr, lang) + local buf = bufnr or api.nvim_get_current_buf() + local config = configs.get_module("textobjects.select") + local lang = lang or parsers.get_buf_lang(buf) + + for mapping, query in pairs(config.keymaps) do + if type(query) == 'table' then + query = query[lang] + elseif not queries.get_query(lang, 'textobjects') then + query = nil + end + if query then + local cmd = ":lua require'nvim-treesitter.textobjects.select'.select_textobject('"..query.."')" + api.nvim_buf_set_keymap(buf, "o", mapping, cmd, {silent = true, noremap = true }) + api.nvim_buf_set_keymap(buf, "v", mapping, cmd, {silent = true, noremap = true }) + end + end +end + +function M.detach(bufnr) + local buf = bufnr or api.nvim_get_current_buf() + local config = configs.get_module("textobjects.select") + local lang = parsers.get_buf_lang(bufnr) + + for mapping, query in pairs(config.keymaps) do + if type(query) == 'table' then + query = query[lang] + elseif not queries.get_query(lang, 'textobjects') then + query = nil + end + if query then + api.nvim_buf_del_keymap(buf, "o", mapping) + api.nvim_buf_del_keymap(buf, "v", mapping) + end + end +end + +return M diff --git a/lua/nvim-treesitter/textobjects/shared.lua b/lua/nvim-treesitter/textobjects/shared.lua new file mode 100644 index 000000000..3a00da427 --- /dev/null +++ b/lua/nvim-treesitter/textobjects/shared.lua @@ -0,0 +1,142 @@ +local api = vim.api +local ts = vim.treesitter + +local parsers = require "nvim-treesitter.parsers" +local queries = require'nvim-treesitter.query' +local ts_utils = require'nvim-treesitter.ts_utils' + +local M = {} + +function M.textobject_at_point(query_string) + local bufnr = vim.api.nvim_get_current_buf() + local lang = parsers.get_buf_lang(bufnr) + if not lang then return end + + local row, col = unpack(vim.api.nvim_win_get_cursor(0)) + row = row - 1 + + local matches = {} + + if string.match(query_string, '^@.*') then + matches = queries.get_capture_matches(bufnr, query_string, 'textobjects') + else + local parser = parsers.get_parser(bufnr, lang) + local root = parser:parse():root() + + local start_row, _, end_row, _ = root:range() + + local query = ts.parse_query(lang, query_string) + for m in queries.iter_prepared_matches(query, root, bufnr, start_row, end_row) do + for _, n in pairs(m) do + if n.node then + table.insert(matches, n) + end + end + end + end + + local match_length + local smallest_range + local earliest_start + + for _, m in pairs(matches) do + if m.node and ts_utils.is_in_node_range(m.node, row, col) then + local length = ts_utils.node_length(m.node) + if not match_length or length < match_length then + smallest_range = m + match_length = length + end + -- for nodes with same length take the one with earliest start + if match_length and length == smallest_range then + local start = m.start + if start then + local _, _, start_byte = m.start.node:start() + if not earliest_start or start_byte < earliest_start then + smallest_range = m + match_length = length + earliest_start = start_byte + end + end + end + end + end + + if smallest_range then + if smallest_range.start then + local start_range = {smallest_range.start.node:range()} + local node_range = {smallest_range.node:range()} + return bufnr, {start_range[1], start_range[2], node_range[3], node_range[4]}, smallest_range.node + else + return bufnr, {smallest_range.node:range()}, smallest_range.node + end + end +end + +function M.get_adjacent(forward, node, query_string, same_parent, overlapping_range_ok, bufnr) + local fn = forward and M.next_textobject or M.previous_textobject + return fn(node, query_string, same_parent, overlapping_range_ok, bufnr) +end + +function M.next_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr) + local node = node or ts_utils.get_node_at_cursor() + local bufnr = bufnr or api.nvim_get_current_buf() + if not node then return end + + local _, _, node_end = node:end_() + local search_start, _ + if overlapping_range_ok then + _, _, search_start = node:start() + else + _, _, search_start = node:end_() + end + local function scoring_function(match) + if match.node == node then return end + if not same_parent or node:parent() == match.node:parent() then + local _, _, start = match.node:start() + local _, _, end_ = match.node:end_() + return start > search_start and end_ >= node_end + end + end + local function filter_function(match) + local _, _, node_start = match.node:start() + return -node_start + end + + local next_node = queries.find_best_match(bufnr, query_string, 'textobjects', scoring_function, filter_function) + + return next_node and next_node.node +end + +function M.previous_textobject(node, query_string, same_parent, overlapping_range_ok, bufnr) + local node = node or ts_utils.get_node_at_cursor() + local bufnr = bufnr or api.nvim_get_current_buf() + if not node then return end + + local _, _, node_start = node:start() + local search_end, _ + if overlapping_range_ok then + _, _, search_end = node:end_() + search_end = search_end + 1 + else + _, _, search_end = node:start() + end + + local function scoring_function(match) + if not same_parent or node:parent() == match.node:parent() then + local _, _, end_ = match.node:end_() + local _, _, start = match.node:start() + return end_ < search_end and start < node_start + end + end + + local function filter_function(match) + local _, _, node_end = match.node:end_() + return node_end + end + + local previous_node = queries.find_best_match(bufnr, query_string, 'textobjects', scoring_function, filter_function) + + return previous_node and previous_node.node +end + +return M diff --git a/lua/nvim-treesitter/textobjects/swap.lua b/lua/nvim-treesitter/textobjects/swap.lua new file mode 100644 index 000000000..bb681fdd0 --- /dev/null +++ b/lua/nvim-treesitter/textobjects/swap.lua @@ -0,0 +1,35 @@ +local ts_utils = require'nvim-treesitter.ts_utils' +local shared = require'nvim-treesitter.textobjects.shared' +local attach = require'nvim-treesitter.textobjects.attach' + +local M = {} + +local function swap_textobject(query_string, direction) + local bufnr, textobject_range, node = shared.textobject_at_point(query_string) + if not node then return end + + local step = direction > 0 and 1 or -1 + local overlapping_range_ok = false + local same_parent = true + for _ = 1, math.abs(direction), step do + local forward = direction > 0 + local adjacent = shared.get_adjacent(forward, node, query_string, same_parent, overlapping_range_ok, bufnr) + ts_utils.swap_nodes(textobject_range, adjacent, bufnr, "yes, set cursor!") + end +end + +function M.swap_next(query_string) + swap_textobject(query_string, 1) +end + +function M.swap_previous(query_string) + swap_textobject(query_string, -1) +end + +local normal_mode_functions = {"swap_next", + "swap_previous"} + +M.attach = attach.make_attach(normal_mode_functions, "swap") +M.deattach = attach.make_detach(normal_mode_functions, "swap") + +return M -- cgit v1.2.3-70-g09d2