lite-xl/data/core/commands/findreplace.lua

304 lines
9.5 KiB
Lua

local core = require "core"
local command = require "core.command"
local config = require "core.config"
local search = require "core.doc.search"
local keymap = require "core.keymap"
local DocView = require "core.docview"
local CommandView = require "core.commandview"
local StatusView = require "core.statusview"
local last_view, last_fn, last_text, last_sel
local case_sensitive = config.find_case_sensitive or false
local find_regex = config.find_regex or false
local found_expression
local function doc()
local is_DocView = core.active_view:is(DocView) and not core.active_view:is(CommandView)
return is_DocView and core.active_view.doc or (last_view and last_view.doc)
end
local function get_find_tooltip()
local rf = keymap.get_binding("find-replace:repeat-find")
local ti = keymap.get_binding("find-replace:toggle-sensitivity")
local tr = keymap.get_binding("find-replace:toggle-regex")
return (find_regex and "[Regex] " or "") ..
(case_sensitive and "[Sensitive] " or "") ..
(rf and ("Press " .. rf .. " to select the next match.") or "") ..
(ti and (" " .. ti .. " toggles case sensitivity.") or "") ..
(tr and (" " .. tr .. " toggles regex find.") or "")
end
local function update_preview(sel, search_fn, text)
local ok, line1, col1, line2, col2 = pcall(search_fn, last_view.doc,
sel[1], sel[2], text, case_sensitive, find_regex)
if ok and line1 and text ~= "" then
last_view.doc:set_selection(line2, col2, line1, col1)
last_view:scroll_to_line(line2, true)
found_expression = true
else
last_view.doc:set_selection(table.unpack(sel))
found_expression = false
end
end
local function insert_unique(t, v)
local n = #t
for i = 1, n do
if t[i] == v then
table.remove(t, i)
break
end
end
table.insert(t, 1, v)
end
local function find(label, search_fn)
last_view, last_sel = core.active_view,
{ core.active_view.doc:get_selection() }
local text = last_view.doc:get_text(table.unpack(last_sel))
found_expression = false
core.status_view:show_tooltip(get_find_tooltip())
core.command_view:enter(label, {
text = text,
select_text = true,
show_suggestions = false,
submit = function(text, item)
insert_unique(core.previous_find, text)
core.status_view:remove_tooltip()
if found_expression then
last_fn, last_text = search_fn, text
else
core.error("Couldn't find %q", text)
last_view.doc:set_selection(table.unpack(last_sel))
last_view:scroll_to_make_visible(table.unpack(last_sel))
end
end,
suggest = function(text)
update_preview(last_sel, search_fn, text)
last_fn, last_text = search_fn, text
return core.previous_find
end,
cancel = function(explicit)
core.status_view:remove_tooltip()
if explicit then
last_view.doc:set_selection(table.unpack(last_sel))
last_view:scroll_to_make_visible(table.unpack(last_sel))
end
end
})
end
local function replace(kind, default, fn)
core.status_view:show_tooltip(get_find_tooltip())
core.command_view:enter("Find To Replace " .. kind, {
text = default,
select_text = true,
show_suggestions = false,
submit = function(old)
insert_unique(core.previous_find, old)
local s = string.format("Replace %s %q With", kind, old)
core.command_view:enter(s, {
text = old,
select_text = true,
show_suggestions = false,
submit = function(new)
core.status_view:remove_tooltip()
insert_unique(core.previous_replace, new)
local results = doc():replace(function(text)
return fn(text, old, new)
end)
local n = 0
for _,v in pairs(results) do
n = n + v
end
core.log("Replaced %d instance(s) of %s %q with %q", n, kind, old, new)
end,
suggest = function() return core.previous_replace end,
cancel = function()
core.status_view:remove_tooltip()
end
})
end,
suggest = function() return core.previous_find end,
cancel = function()
core.status_view:remove_tooltip()
end
})
end
local function has_selection()
return core.active_view:is(DocView) and core.active_view.doc:has_selection()
end
local function has_unique_selection()
if not doc() then return false end
local text = nil
for idx, line1, col1, line2, col2 in doc():get_selections(true, true) do
if line1 == line2 and col1 == col2 then return false end
local selection = doc():get_text(line1, col1, line2, col2)
if text ~= nil and text ~= selection then return false end
text = selection
end
return text ~= nil
end
local function is_in_selection(line, col, l1, c1, l2, c2)
if line < l1 or line > l2 then return false end
if line == l1 and col <= c1 then return false end
if line == l2 and col > c2 then return false end
return true
end
local function is_in_any_selection(line, col)
for idx, l1, c1, l2, c2 in doc():get_selections(true, false) do
if is_in_selection(line, col, l1, c1, l2, c2) then return true end
end
return false
end
local function select_add_next(all)
local il1, ic1 = doc():get_selection(true)
for idx, l1, c1, l2, c2 in doc():get_selections(true, true) do
local text = doc():get_text(l1, c1, l2, c2)
repeat
l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true })
if l1 == il1 and c1 == ic1 then break end
if l2 and (all or not is_in_any_selection(l2, c2)) then
doc():add_selection(l2, c2, l1, c1)
if not all then
core.active_view:scroll_to_make_visible(l2, c2)
return
end
end
until not all or not l2
if all then break end
end
end
local function select_next(reverse)
local l1, c1, l2, c2 = doc():get_selection(true)
local text = doc():get_text(l1, c1, l2, c2)
if reverse then
l1, c1, l2, c2 = search.find(doc(), l1, c1, text, { wrap = true, reverse = true })
else
l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true })
end
if l2 then doc():set_selection(l2, c2, l1, c1) end
end
---@param in_selection? boolean whether to replace in the selections only, or in the whole file.
local function find_replace(in_selection)
local l1, c1, l2, c2 = doc():get_selection()
local selected_text = ""
if not in_selection then
selected_text = doc():get_text(l1, c1, l2, c2)
doc():set_selection(l2, c2, l2, c2)
end
replace("Text", l1 == l2 and selected_text or "", function(text, old, new)
if not find_regex then
return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil)
end
local result, matches = regex.gsub(regex.compile(old, "m"), text, new)
return result, matches
end)
end
command.add(has_unique_selection, {
["find-replace:select-next"] = select_next,
["find-replace:select-previous"] = function() select_next(true) end,
["find-replace:select-add-next"] = select_add_next,
["find-replace:select-add-all"] = function() select_add_next(true) end
})
command.add("core.docview!", {
["find-replace:find"] = function()
find("Find Text", function(doc, line, col, text, case_sensitive, find_regex, find_reverse)
local opt = { wrap = true, no_case = not case_sensitive, regex = find_regex, reverse = find_reverse }
return search.find(doc, line, col, text, opt)
end)
end,
["find-replace:replace"] = function()
find_replace()
end,
["find-replace:replace-in-selection"] = function()
find_replace(true)
end,
["find-replace:replace-symbol"] = function()
local first = ""
if doc():has_selection() then
local text = doc():get_text(doc():get_selection())
first = text:match(config.symbol_pattern) or ""
end
replace("Symbol", first, function(text, old, new)
local n = 0
local res = text:gsub(config.symbol_pattern, function(sym)
if old == sym then
n = n + 1
return new
end
end)
return res, n
end)
end,
})
local function valid_for_finding()
return core.active_view:is(DocView) or core.active_view:is(CommandView)
end
command.add(valid_for_finding, {
["find-replace:repeat-find"] = function()
if not last_fn then
core.error("No find to continue from")
else
local sl1, sc1, sl2, sc2 = doc():get_selection(true)
local line1, col1, line2, col2 = last_fn(doc(), sl1, sc2, last_text, case_sensitive, find_regex, false)
if line1 then
doc():set_selection(line2, col2, line1, col1)
last_view:scroll_to_line(line2, true)
else
core.error("Couldn't find %q", last_text)
end
end
end,
["find-replace:previous-find"] = function()
if not last_fn then
core.error("No find to continue from")
else
local sl1, sc1, sl2, sc2 = doc():get_selection(true)
local line1, col1, line2, col2 = last_fn(doc(), sl1, sc1, last_text, case_sensitive, find_regex, true)
if line1 then
doc():set_selection(line2, col2, line1, col1)
last_view:scroll_to_line(line2, true)
else
core.error("Couldn't find %q", last_text)
end
end
end,
})
command.add("core.commandview", {
["find-replace:toggle-sensitivity"] = function()
case_sensitive = not case_sensitive
core.status_view:show_tooltip(get_find_tooltip())
if last_sel then update_preview(last_sel, last_fn, last_text) end
end,
["find-replace:toggle-regex"] = function()
find_regex = not find_regex
core.status_view:show_tooltip(get_find_tooltip())
if last_sel then update_preview(last_sel, last_fn, last_text) end
end
})