Skip to content

Commit

Permalink
feat: introduce modifiers
Browse files Browse the repository at this point in the history
  • Loading branch information
gbprod committed Nov 14, 2023
1 parent cf35db5 commit 5fa23e6
Show file tree
Hide file tree
Showing 9 changed files with 480 additions and 24 deletions.
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

0 comments on commit 5fa23e6

Please sign in to comment.