Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce modifiers #66

Merged
merged 1 commit into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
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
76 changes: 69 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Substitute comes with the following defaults:
on_substitute = nil,
yank_substituted_text = false,
preserve_cursor_position = false,
modifiers = nil,
highlight_substituted_text = {
enabled = true,
timer = 500,
Expand Down Expand Up @@ -113,10 +114,12 @@ Each functions (`operator`, `line`, `eol` and `visual`) are configurable:

```lua
lua require('substitute').operator({
count = 1, -- number of substitutions
register = "a", -- register used for substitution
motion = "iw", -- only available for `operator`, this will automatically use
-- this operator for substitution instead of asking for.
count = 1, -- number of substitutions
register = "a", -- register used for substitution
motion = "iw", -- only available for `operator`, this will automatically use
-- this operator for substitution instead of asking for.
modifiers = nil, -- this allows to modify substitued text, will override the default
-- configuration (see below)
})
```

Expand All @@ -134,24 +137,83 @@ Default : `false`

If `true`, when performing a substitution, substitued text is pushed into the default register.

### `highlight_substituted_text.enabled`
#### `highlight_substituted_text.enabled`

Default : `true`

If `true` will temporary highlight substitued text.

### `highlight_substituted_text.timer`
#### `highlight_substituted_text.timer`

Default : `500`

Define the duration of highlight.

### `preserve_cursor_position`
#### `preserve_cursor_position`

Default : `false`

If `true`, the cursor position will be preserved when performing a substitution.

#### `modifiers`

Default : `nil`

Could be a function or a table of transformations that will be called to modify substitued text. See modifiers section below.

### ➰ Modifiers

Modifiers are used to modify the text before substitution is performed. You can chain those modifiers or even use a function to dynamicly choose modifier depending on the context.

Available modifiers are:

- `linewise` : will create a new line for substitution ;
- `reindent` : will reindent substitued text ;
- `trim` : will trim substitued text ;
- `join` : will join lines of substitued text.

### Examples

If you want to create a new line for substitution and reindent, you can use:

```lua
require('substitute').operator({
modifiers = { 'linewise', 'reindent' },
})
```

If you want to trim and join lines of substitued text, you can use:

```lua
require('substitute').operator({
modifiers = { 'join', 'trim' },
})
```

If you want to trim text but only if you substitute text in a charwise motion, you can use:

```lua
require('substitute').operator({
modifiers = function(state)
if state.vmode == 'char' then
return { 'trim' }
end
end,
})
```

If you always want to reindent text when making a linewise substitution, you can use:

```lua
require('substitute').operator({
modifiers = function(state)
if state.vmode == 'line' then
return { 'reindent' }
end
end,
})
```

### 🤝 Integration

<details>
Expand Down
31 changes: 21 additions & 10 deletions lua/substitute.lua
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function substitute.operator(options)
options = options or {}
substitute.state.register = options.register or vim.v.register
substitute.state.count = options.count or (vim.v.count > 0 and vim.v.count or 1)
substitute.state.modifiers = options.modifiers or nil
if config.options.preserve_cursor_position then
substitute.state.curpos = vim.api.nvim_win_get_cursor(0)
end
Expand All @@ -35,24 +36,30 @@ end

function substitute.operator_callback(vmode)
local marks = utils.get_marks(0, vmode)

-- print(vim.inspect(marks))
substitute.state.vmode = vmode
substitute.state.marks = marks

local substitued_text = utils.text(0, marks.start, marks.finish, vmode)

local regcontents = vim.fn.getreg(substitute.state.register)
local regtype = vim.fn.getregtype(substitute.state.register)
local replacement = vim.split(regcontents:rep(substitute.state.count):gsub("\n$", ""), "\n")
local doSubstitution = function(state, _)
local regcontents = vim.fn.getreg(state.register)
local regtype = vim.fn.getregtype(state.register)
local replacement = vim.split(regcontents:rep(substitute.state.count):gsub("\n$", ""), "\n")

local subs_marks = utils.substitute_text(0, marks.start, marks.finish, vmode, replacement, regtype)
local subs_marks = utils.substitute_text(0, marks.start, marks.finish, vmode, replacement, regtype)

vim.api.nvim_buf_set_mark(0, "[", subs_marks[1].start.row, subs_marks[1].start.col, {})
vim.api.nvim_buf_set_mark(0, "]", subs_marks[#subs_marks].finish.row, subs_marks[#subs_marks].finish.col - 1, {})
vim.api.nvim_buf_set_mark(0, "[", subs_marks[1].start.row, subs_marks[1].start.col, {})
vim.api.nvim_buf_set_mark(0, "]", subs_marks[#subs_marks].finish.row, subs_marks[#subs_marks].finish.col - 1, {})

if config.options.highlight_substituted_text.enabled then
substitute.highlight_substituted_text(subs_marks)
if config.options.highlight_substituted_text.enabled then
substitute.highlight_substituted_text(subs_marks)
end
end

local modifier = config.get_modifiers(substitute.state) or doSubstitution

modifier(substitute.state, doSubstitution)

if config.options.yank_substituted_text then
vim.fn.setreg(utils.get_default_register(), table.concat(substitued_text, "\n"), utils.get_register_type(vmode))
end
Expand All @@ -78,6 +85,7 @@ function substitute.line(options)
motion = count .. "_",
count = 1,
register = options.register or vim.v.register,
modifiers = options.modifiers or nil,
})
end

Expand All @@ -87,13 +95,16 @@ function substitute.eol(options)
motion = "$",
register = options.register or vim.v.register,
count = options.count or (vim.v.count > 0 and vim.v.count or 1),
modifiers = options.modifiers or nil,
})
end

function substitute.visual(options)
options = options or {}
substitute.state.register = options.register or vim.v.register
substitute.state.count = options.count or (vim.v.count > 0 and vim.v.count or 1)
substitute.state.modifiers = options.modifiers or nil

vim.o.operatorfunc = "v:lua.require'substitute'.operator_callback"
vim.api.nvim_feedkeys("g@`<", "ni", false)
end
Expand Down
13 changes: 13 additions & 0 deletions lua/substitute/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ function config.setup(options)
on_substitute = nil,
yank_substituted_text = false,
preserve_cursor_position = false,
modifiers = nil,
highlight_substituted_text = {
enabled = true,
timer = 500,
Expand Down Expand Up @@ -45,4 +46,16 @@ function config.get_exchange(overrides)
}
end

function config.get_modifiers(state)
if type(state.modifiers) == "function" then
return require("substitute.modifiers").build(state.modifiers(state))
end

if type(state.modifiers) == "table" then
return require("substitute.modifiers").build(state.modifiers)
end

return config.options.modifiers
end

return config
86 changes: 86 additions & 0 deletions lua/substitute/modifiers.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
local modifiers = {}

function modifiers.linewise(next)
return function(state, callback)
local body = vim.fn.getreg(state.register)
local type = vim.fn.getregtype(state.register)

if state.vmode ~= "line" then
-- we add a newline at the end nly if we don't replace to the end of a
-- line and if we don't replace to line mode
local should_wrap = type ~= "V" and state.marks.finish.col + 1 < vim.fn.getline(state.marks.finish.row):len()
vim.fn.setreg(state.register, string.format("\n%s\n%s", body, should_wrap and "\n" or ""), type)
end

if nil == next then
callback(state)
else
next(state, callback)
end

vim.fn.setreg(state.register, body, type)
end
end

function modifiers.trim(next)
return function(state, callback)
local body = vim.fn.getreg(state.register)

local reformated_body = body:gsub("^%s*", ""):gsub("%s*$", "")
vim.fn.setreg(state.register, reformated_body, vim.fn.getregtype(state.register))

if nil == next then
callback(state)
else
next(state, callback)
end

vim.fn.setreg(state.register, body, vim.fn.getregtype(state.register))
end
end

function modifiers.join(next)
return function(state, callback)
local body = vim.fn.getreg(state.register)

local reformated_body = body:gsub("%s*\r?\n%s*", " ")
vim.fn.setreg(state.register, reformated_body, vim.fn.getregtype(state.register))

if nil == next then
callback(state)
else
next(state, callback)
end

vim.fn.setreg(state.register, body, vim.fn.getregtype(state.register))
end
end

function modifiers.reindent(next)
return function(state, callback)
if nil == next then
callback(state)
else
next(state, callback)
end

local cursor_pos = vim.api.nvim_win_get_cursor(0)
vim.cmd("silent '[,']normal! ==")
vim.api.nvim_win_set_cursor(0, cursor_pos)
end
end

function modifiers.build(chain)
if nil == chain then
return nil
end

local modifier = nil
for index = #chain, 1, -1 do
modifier = modifiers[chain[index]](modifier)
end

return modifier
end

return modifiers
2 changes: 2 additions & 0 deletions lua/substitute/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ end

function utils.substitute_text(bufnr, start, finish, regtype, replacement, replacement_regtype)
regtype = utils.get_register_type(regtype)
replacement_regtype = utils.get_register_type(replacement_regtype)

if "l" == regtype then
vim.api.nvim_buf_set_lines(bufnr, start.row - 1, finish.row, false, replacement)
Expand Down Expand Up @@ -97,6 +98,7 @@ function utils.substitute_text(bufnr, start, finish, regtype, replacement, repla
vim.api.nvim_buf_set_text(bufnr, start.row - 1, start.col, start.row - 1, start.col, replacement)
else
local current_row_len = vim.fn.getline(finish.row):len()

vim.api.nvim_buf_set_text(
bufnr,
start.row - 1,
Expand Down
14 changes: 7 additions & 7 deletions spec/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ function M.setup()

M.load("nvim-lua/plenary.nvim")

vim.keymap.set("n", "ss", "<cmd>lua require('substitute').line()<cr>", { noremap = true })
vim.keymap.set("n", "S", "<cmd>lua require('substitute').eol()<cr>", { noremap = true })
vim.keymap.set("n", "s", "<cmd>lua require('substitute').operator()<cr>", { noremap = true })
vim.keymap.set("x", "s", "<cmd>lua require('substitute').visual()<cr>", { noremap = true })
vim.keymap.set("n", "ss", require("substitute").line, { noremap = true })
vim.keymap.set("n", "S", require("substitute").eol, { noremap = true })
vim.keymap.set("n", "s", require("substitute").operator, { noremap = true })
vim.keymap.set("x", "s", require("substitute").visual, { noremap = true })

vim.keymap.set("n", "<leader>s", "<cmd>lua require('substitute.range').operator()<cr>", { noremap = true })
vim.keymap.set("n", "<leader>s", require("substitute.range").operator, { noremap = true })

vim.keymap.set("n", "sx", "<cmd>lua require('substitute.exchange').operator()<cr>", { noremap = true })
vim.keymap.set("x", "X", "<cmd>lua require('substitute.exchange').visual()<cr>", { noremap = true })
vim.keymap.set("n", "sx", require("substitute.exchange").operator, { noremap = true })
vim.keymap.set("x", "X", require("substitute.exchange").visual, { noremap = true })
end

M.setup()
59 changes: 59 additions & 0 deletions spec/substitute/modifiers/config_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
local substitute = require("substitute")

local function execute_keys(feedkeys)
local keys = vim.api.nvim_replace_termcodes(feedkeys, true, false, true)
vim.api.nvim_feedkeys(keys, "x", false)
end

local function get_buf_lines()
return vim.api.nvim_buf_get_lines(0, 0, -1, true)
end

local buf
describe("Substitute modifiers", function()
before_each(function()
substitute.setup()

buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_command("buffer " .. buf)
end)

it("should be taken from a function", function()
vim.api.nvim_buf_set_lines(buf, 0, -1, true, { "Lorem", "ipsum", "dolor", "sit", "amet" })

vim.keymap.set({ "n", "x" }, "]s", function()
require("substitute").operator({
modifiers = function(_)
return { "linewise" }
end,
})
end, { noremap = true })

execute_keys("lly2l")
execute_keys("j")
execute_keys("]s2l")

assert.are.same({ "Lorem", "ip", "re", "m", "dolor", "sit", "amet" }, get_buf_lines())
end)

it("could be conditionnal", function()
vim.api.nvim_buf_set_lines(buf, 0, -1, true, { " Lorem ", "ipsum", "dolor", "sit", "amet" })

vim.keymap.set({ "n", "x" }, "]s", function()
require("substitute").operator({
modifiers = function(state)
return state.vmode == "char" and { "trim" } or { "linewise" }
end,
})
end, { noremap = true })

execute_keys("yy")
execute_keys("jll")
execute_keys("]s2l")

execute_keys("jV")
execute_keys("]s")

assert.are.same({ " Lorem ", "ipLoremm", " Lorem ", "sit", "amet" }, get_buf_lines())
end)
end)
Loading