feat: Improve `move_cursor`. (#334)

Adds "sticky" option for `move_cursor`, making the cursor "stick" to the text as the buffer gets modified.
This commit is contained in:
Kyle Chui 2024-06-08 14:29:15 -07:00 committed by GitHub
parent ae876ab0f8
commit ef592b5c4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 298 additions and 19 deletions

View File

@ -613,16 +613,29 @@ configured separately. The default highlight group used is `Visual`:
--------------------------------------------------------------------------------
3.6. Cursor *nvim-surround.config.move_cursor*
By default, when a surround action is performed, the cursor moves to the
beginning of the action.
By default (or when `move_cursor = "begin"`), when a surround action is
performed, the cursor moves to the beginning of the action.
Old text Command New text ~
some_t*ext ysiw[ *[ some_text ]
another { sample *} ds{ another *sample
(hello* world) csbB *{hello world}
This behavior can be disabled by setting `move_cursor = false` in one of the
setup functions.
If `move_cursor` is set to `"sticky"`, the cursor will "stick" to the current
character, and move with the text as the buffer changes.
Old text Command New text ~
some_t*ext ysiw[ [ some_t*ext ]
another { sample *} ds{ another sampl*e
(hello* world) csbffoo<CR> foo(hello* world)
If `move_cursor` is set to `false`, the cursor won't move at all, regardless
of how the buffer changes.
Old text Command New text ~
some_t*ext ysiw[ [ some_*text ]
another { *sample } ds{ another sa*mple
(hello* world) csbffoo<CR> foo(he*llo world)
--------------------------------------------------------------------------------
3.7. Indentation *nvim-surround.config.indent_lines*

View File

@ -35,7 +35,7 @@
---@field surrounds table<string, surround>
---@field aliases table<string, string|string[]>
---@field highlight { duration: integer }
---@field move_cursor false|"begin"|"end"
---@field move_cursor false|"begin"|"sticky"
---@field indent_lines function
--[====================================================================================================================[
@ -58,5 +58,5 @@
---@field surrounds? table<string, false|user_surround>
---@field aliases? table<string, false|string|string[]>
---@field highlight? { duration: false|integer }
---@field move_cursor? false|"begin"|"end"
---@field move_cursor? false|"begin"|"sticky"
---@field indent_lines? false|function

View File

@ -2,6 +2,11 @@ local config = require("nvim-surround.config")
local M = {}
M.namespace = {
highlight = vim.api.nvim_create_namespace("nvim-surround-highlight"),
extmark = vim.api.nvim_create_namespace("nvim-surround-extmark"),
}
--[====================================================================================================================[
Cursor helper functions
--]====================================================================================================================]
@ -24,11 +29,12 @@ M.set_curpos = function(pos)
end
-- Move the cursor to a location in the buffer, depending on the `move_cursor` setting.
---@param pos { first_pos: position, old_pos: position } Various positions in the buffer.
---@param pos { first_pos: position, sticky_pos: position, old_pos: position } Various positions in the buffer.
M.restore_curpos = function(pos)
-- TODO: Add a `last_pos` field for if `move_cursor` is set to "end"
if config.get_opts().move_cursor == "begin" then
M.set_curpos(pos.first_pos)
elseif config.get_opts().move_cursor == "sticky" then
M.set_curpos(pos.sticky_pos)
elseif not config.get_opts().move_cursor then
M.set_curpos(pos.old_pos)
end
@ -117,6 +123,29 @@ M.set_operator_marks = function(motion)
M.set_mark(">", visual_marks[2])
end
-- Gets extmark position for the current buffer.
---@param extmark integer The extmark ID number.
---@return position @The position of the extmark in the buffer.
---@nodiscard
M.get_extmark = function(extmark)
local pos = vim.api.nvim_buf_get_extmark_by_id(0, M.namespace.extmark, extmark, {})
return { pos[1] + 1, pos[2] + 1 }
end
-- Creates an extmark for the given position.
---@param pos position The position in the buffer.
---@return integer @The extmark ID.
---@nodiscard
M.set_extmark = function(pos)
return vim.api.nvim_buf_set_extmark(0, M.namespace.extmark, pos[1] - 1, pos[2] - 1, {})
end
-- Deletes an extmark from the buffer.
---@param extmark integer The extmark ID number.
M.del_extmark = function(extmark)
vim.api.nvim_buf_del_extmark(0, M.namespace.extmark, extmark)
end
--[====================================================================================================================[
Byte indexing helper functions
--]====================================================================================================================]
@ -257,11 +286,10 @@ M.highlight_selection = function(selection)
if not selection then
return
end
local namespace = vim.api.nvim_create_namespace("NvimSurround")
vim.highlight.range(
0,
namespace,
M.namespace.highlight,
"NvimSurroundHighlight",
{ selection.first_pos[1] - 1, selection.first_pos[2] - 1 },
{ selection.last_pos[1] - 1, selection.last_pos[2] - 1 },
@ -273,8 +301,7 @@ end
-- Clears all nvim-surround highlights for the buffer.
M.clear_highlights = function()
local namespace = vim.api.nvim_create_namespace("NvimSurround")
vim.api.nvim_buf_clear_namespace(0, namespace, 0, -1)
vim.api.nvim_buf_clear_namespace(0, M.namespace.highlight, 0, -1)
-- Force the screen to clear the highlight immediately
vim.cmd.redraw()
end

View File

@ -64,12 +64,16 @@ M.normal_surround = function(args)
local first_pos = args.selection.first_pos
local last_pos = { args.selection.last_pos[1], args.selection.last_pos[2] + 1 }
local sticky_mark = buffer.set_extmark(M.normal_curpos)
buffer.insert_text(last_pos, args.delimiters[2])
buffer.insert_text(first_pos, args.delimiters[1])
buffer.restore_curpos({
first_pos = first_pos,
sticky_pos = buffer.get_extmark(sticky_mark),
old_pos = M.normal_curpos,
})
buffer.del_extmark(sticky_mark)
if args.line_mode then
config.get_opts().indent_lines(first_pos[1], last_pos[1] + #args.delimiters[1] + #args.delimiters[2] - 2)
@ -92,6 +96,7 @@ M.visual_surround = function(args)
return
end
local sticky_mark = buffer.set_extmark(args.curpos)
if vim.fn.visualmode() == "\22" then -- Visual block mode case (add delimiters to every line)
if vim.o.selection == "exclusive" then
last_pos[2] = last_pos[2] - 1
@ -144,8 +149,10 @@ M.visual_surround = function(args)
config.get_opts().indent_lines(first_pos[1], last_pos[1] + #delimiters[1] + #delimiters[2] - 2)
buffer.restore_curpos({
first_pos = first_pos,
sticky_pos = buffer.get_extmark(sticky_mark),
old_pos = args.curpos,
})
buffer.del_extmark(sticky_mark)
end
-- Delete a surrounding delimiter pair, if it exists.
@ -165,17 +172,21 @@ M.delete_surround = function(args)
local selections = utils.get_nearest_selections(args.del_char, "delete")
if selections then
local sticky_mark = buffer.set_extmark(args.curpos)
-- Delete the right selection first to ensure selection positions are correct
buffer.delete_selection(selections.right)
buffer.delete_selection(selections.left)
config.get_opts().indent_lines(
selections.left.first_pos[1],
selections.left.first_pos[1] + selections.right.first_pos[1] - selections.left.last_pos[1]
)
buffer.restore_curpos({
first_pos = selections.left.first_pos,
sticky_pos = buffer.get_extmark(sticky_mark),
old_pos = args.curpos,
})
buffer.del_extmark(sticky_mark)
end
cache.set_callback("v:lua.require'nvim-surround'.delete_callback")
@ -221,13 +232,16 @@ M.change_surround = function(args)
selections.right.first_pos[2] = space_end + 1
end
local sticky_mark = buffer.set_extmark(args.curpos)
-- Change the right selection first to ensure selection positions are correct
buffer.change_selection(selections.right, delimiters[2])
buffer.change_selection(selections.left, delimiters[1])
buffer.restore_curpos({
first_pos = selections.left.first_pos,
sticky_pos = buffer.get_extmark(sticky_mark),
old_pos = args.curpos,
})
buffer.del_extmark(sticky_mark)
if args.line_mode then
local first_pos = selections.left.first_pos

View File

@ -1,5 +1,6 @@
local cr = vim.api.nvim_replace_termcodes("<CR>", true, false, true)
local esc = vim.api.nvim_replace_termcodes("<Esc>", true, false, true)
local ctrl_v = vim.api.nvim_replace_termcodes("<C-v>", true, false, true)
local get_curpos = function()
local curpos = vim.api.nvim_win_get_cursor(0)
return { curpos[1], curpos[2] + 1 }
@ -7,12 +8,18 @@ end
local set_curpos = function(pos)
vim.api.nvim_win_set_cursor(0, { pos[1], pos[2] - 1 })
end
local check_curpos = function(pos)
assert.are.same({ pos[1], pos[2] - 1 }, vim.api.nvim_win_get_cursor(0))
end
local set_lines = function(lines)
vim.api.nvim_buf_set_lines(0, 0, -1, false, lines)
end
local check_lines = function(lines)
assert.are.same(lines, vim.api.nvim_buf_get_lines(0, 0, -1, false))
end
local get_extmarks = function()
return vim.api.nvim_buf_get_extmarks(0, require("nvim-surround.buffer").namespace.extmark, 0, -1, {})
end
describe("configuration", function()
before_each(function()
@ -21,7 +28,7 @@ describe("configuration", function()
end)
it("can define own add mappings", function()
require("nvim-surround").setup({
require("nvim-surround").buffer_setup({
surrounds = {
["1"] = { add = { "1", "1" } },
["2"] = { add = { "2", { "2" } } },
@ -75,7 +82,7 @@ describe("configuration", function()
end)
it("can define and use 'interpreted' multi-byte mappings", function()
require("nvim-surround").setup({
require("nvim-surround").buffer_setup({
surrounds = {
-- interpreted multi-byte
["<M-]>"] = {
@ -95,7 +102,7 @@ describe("configuration", function()
end)
it("default deletes using invalid_key_behavior for an 'interpreted' multi-byte mapping", function()
require("nvim-surround").setup({
require("nvim-surround").buffer_setup({
surrounds = {
-- interpreted multi-byte
["<C-q>"] = {
@ -114,7 +121,7 @@ describe("configuration", function()
end)
it("can disable surrounds", function()
require("nvim-surround").setup({
require("nvim-surround").buffer_setup({
surrounds = {
["("] = false,
},
@ -130,7 +137,7 @@ describe("configuration", function()
end)
it("can change invalid_key_behavior", function()
require("nvim-surround").setup({
require("nvim-surround").buffer_setup({
surrounds = {
invalid_key_behavior = {
add = function(char)
@ -150,7 +157,7 @@ describe("configuration", function()
end)
it("can disable indent_lines", function()
require("nvim-surround").setup({
require("nvim-surround").buffer_setup({
indent_lines = false,
})
@ -166,7 +173,7 @@ describe("configuration", function()
end)
it("can disable invalid_key_behavior", function()
require("nvim-surround").setup({
require("nvim-surround").buffer_setup({
surrounds = {
invalid_key_behavior = false,
},
@ -270,6 +277,224 @@ describe("configuration", function()
})
end)
it("can make the cursor 'stick' to the text (normal)", function()
require("nvim-surround").buffer_setup({
move_cursor = "sticky",
surrounds = {
["c"] = { add = { "singleline", "surr" } },
["d"] = { add = { { "multiline", "f" }, "" } },
["e"] = { add = { { "multiline", "f" }, { "", "shouldbethislength" } } },
["f"] = { add = { "singleline", { "", "multilinehere" } } },
},
})
-- Sticks to the text if the cursor is inside the selection
set_lines({
"this is a line",
})
set_curpos({ 1, 9 })
vim.cmd("normal ysiwc")
check_curpos({ 1, 19 })
set_lines({
"this is a line",
})
set_curpos({ 1, 4 })
vim.cmd("normal ysiwd")
check_curpos({ 2, 5 })
set_lines({
"this is another line",
})
set_curpos({ 1, 14 })
vim.cmd("normal ysiwe")
check_curpos({ 2, 7 })
set_lines({
"this is a line",
})
set_curpos({ 1, 9 })
vim.cmd("normal ysiwf")
check_curpos({ 1, 19 })
-- Doesn't move if the cursor is before the selection
set_lines({
"this 'is' a line",
})
set_curpos({ 1, 2 })
vim.cmd("normal ysa'c")
vim.cmd("normal ysa'd")
vim.cmd("normal ysa'e")
vim.cmd("normal ysa'f")
check_curpos({ 1, 2 })
assert.are.same(get_extmarks(), {})
end)
it("can make the cursor 'stick' to the text (visual)", function()
require("nvim-surround").buffer_setup({
move_cursor = "sticky",
})
set_lines({
"this is a line",
})
set_curpos({ 1, 9 })
vim.cmd("normal vllS'")
check_curpos({ 1, 12 })
set_lines({
"this is a line",
"with some more text",
})
set_curpos({ 1, 6 })
vim.cmd("normal vjeSb")
check_curpos({ 2, 9 })
set_lines({
"this is a line",
"with some more text",
})
set_curpos({ 1, 6 })
vim.cmd("normal vjeoSb")
check_curpos({ 1, 7 })
assert.are.same(get_extmarks(), {})
end)
it("can make the cursor 'stick' to the text (visual line)", function()
require("nvim-surround").buffer_setup({
move_cursor = "sticky",
})
set_lines({
"this is a line",
})
set_curpos({ 1, 9 })
vim.cmd("normal VSb")
check_curpos({ 2, 9 })
set_lines({
"this is a line",
"with some more text",
})
set_curpos({ 1, 6 })
vim.cmd("normal VjStdiv" .. cr)
check_curpos({ 3, 6 })
assert.are.same(get_extmarks(), {})
end)
it("can make the cursor 'stick' to the text (visual block)", function()
require("nvim-surround").buffer_setup({
move_cursor = "sticky",
surrounds = {
["x"] = {
add = { { "|", "" }, { "", "|" } },
},
},
})
set_lines({
"this is a line",
"this is another line",
})
set_curpos({ 1, 5 })
vim.cmd("normal! " .. ctrl_v .. "jf ")
vim.cmd("normal Sb")
check_curpos({ 2, 9 })
set_lines({
"this is a line",
"this is another line",
"some more random text",
})
set_curpos({ 1, 4 })
vim.cmd("normal! " .. ctrl_v .. "jjww")
vim.cmd("normal Sx")
set_curpos({ 8, 8 })
assert.are.same(get_extmarks(), {})
end)
it("can make the cursor 'stick' to the text (delete)", function()
require("nvim-surround").buffer_setup({
move_cursor = "sticky",
})
set_lines({
"func_name(foobar)",
})
set_curpos({ 1, 14 })
vim.cmd("normal dsf")
check_curpos({ 1, 4 })
set_lines({
"<div id='foobar'>",
" hello",
"</div>",
})
set_curpos({ 2, 7 })
vim.cmd("normal dst")
check_curpos({ 2, 7 })
set_lines({
"hello 'world'",
})
set_curpos({ 1, 2 })
vim.cmd("normal dsq")
check_curpos({ 1, 2 })
set_lines({
"func(hello) world",
})
set_curpos({ 1, 14 })
vim.cmd("normal dsf")
check_curpos({ 1, 8 })
assert.are.same(get_extmarks(), {})
end)
it("can make the cursor 'stick' to the text (change)", function()
require("nvim-surround").buffer_setup({
move_cursor = "sticky",
})
set_lines({
"func_name(foobar)",
})
set_curpos({ 1, 14 })
vim.cmd("normal csff" .. cr)
check_curpos({ 1, 6 })
set_lines({
"<div id='foobar'>",
" hello",
"</div>",
})
set_curpos({ 2, 7 })
vim.cmd("normal csth1" .. cr)
check_curpos({ 2, 7 })
vim.cmd("normal csTbutton" .. cr)
check_curpos({ 2, 7 })
set_lines({
"hello 'world'",
})
set_curpos({ 1, 2 })
vim.cmd("normal csqffoobar" .. cr)
check_curpos({ 1, 2 })
set_lines({
"<div className='container'>hello</div> world",
})
set_curpos({ 1, 41 })
vim.cmd("normal csTb" .. cr)
check_curpos({ 1, 15 })
assert.are.same(get_extmarks(), {})
end)
it("can partially define surrounds", function()
require("nvim-surround").buffer_setup({
surrounds = {