aboutsummaryrefslogtreecommitdiff
path: root/plugins/search_ui.lua
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/search_ui.lua')
-rw-r--r--plugins/search_ui.lua979
1 files changed, 979 insertions, 0 deletions
diff --git a/plugins/search_ui.lua b/plugins/search_ui.lua
new file mode 100644
index 0000000..e030184
--- /dev/null
+++ b/plugins/search_ui.lua
@@ -0,0 +1,979 @@
+-- mod-version:3
+--
+-- Replacement for the find/replace and project search CommandView
+---interface using Widgets with some extra features.
+-- @copyright Jefferson Gonzalez <jgmdev@gmail.com>
+-- @license MIT
+--
+local core = require "core"
+local config = require "core.config"
+local common = require "core.common"
+local command = require "core.command"
+local keymap = require "core.keymap"
+local search = require "core.doc.search"
+local projectsearch = require "plugins.projectsearch"
+local CommandView = require "core.commandview"
+local DocView = require "core.docview"
+local Widget = require "libraries.widget"
+local Button = require "libraries.widget.button"
+local CheckBox = require "libraries.widget.checkbox"
+local Line = require "libraries.widget.line"
+local Label = require "libraries.widget.label"
+local TextBox = require "libraries.widget.textbox"
+local SelectBox = require "libraries.widget.selectbox"
+local FilePicker = require "libraries.widget.filepicker"
+
+---@class config.plugins.search_ui
+---@field replace_core_find boolean
+---@field position "right" | "bottom"
+config.plugins.search_ui = common.merge({
+ replace_core_find = true,
+ position = "bottom",
+ config_spec = {
+ name = "Search User Interface",
+ {
+ label = "Replace Core Find",
+ description = "Replaces the core find view when using the find shortcut.",
+ path = "replace_core_find",
+ type = "toggle",
+ default = true
+ },
+ {
+ label = "Position",
+ description = "Location of search interface.",
+ path = "position",
+ type = "selection",
+ default = "bottom",
+ values = {
+ { "Top", "top" },
+ { "Right", "right" },
+ { "Bottom", "bottom" }
+ }
+ }
+ }
+}, config.plugins.search_ui)
+
+---@type core.docview
+local doc_view = nil
+
+---@type widget
+local widget = Widget(nil, false)
+widget.name = "Search and Replace"
+widget:set_border_width(0)
+widget.scrollable = true
+widget:hide()
+widget.init_size = true
+
+---@type widget.label
+local label = Label(widget, "Find and Replace")
+label:set_position(10, 10)
+
+---@type widget.line
+local line = Line(widget)
+line:set_position(0, label:get_bottom() + 10)
+
+---@type widget.textbox
+local findtext = TextBox(widget, "", "search...")
+findtext:set_position(10, line:get_bottom() + 10)
+findtext:set_tooltip("Text to search")
+
+---@type widget.textbox
+local replacetext = TextBox(widget, "", "replacement...")
+replacetext:set_position(10, findtext:get_bottom() + 10)
+replacetext:set_tooltip("Text to replace")
+
+---@type widget.button
+local findprev = Button(widget, "")
+findprev:set_icon("<")
+findprev:set_position(10, replacetext:get_bottom() + 10)
+findprev:set_tooltip("Find previous")
+
+---@type widget.button
+local findnext = Button(widget, "")
+findnext:set_icon(">")
+findnext:set_position(findprev:get_right() + 5, replacetext:get_bottom() + 10)
+findnext:set_tooltip("Find next")
+
+---@type widget.button
+local findproject = Button(widget, "Find")
+findproject:set_icon("L")
+findproject:set_position(findprev:get_right() + 5, replacetext:get_bottom() + 10)
+findproject:set_tooltip("Find in project")
+findproject:hide()
+
+---@type widget.button
+local replace = Button(widget, "Replace")
+replace:set_position(10, findnext:get_bottom() + 10)
+replace:set_tooltip("Replace all matching results")
+
+---@type widget.line
+local line_options = Line(widget)
+line_options:set_position(0, replace:get_bottom() + 10)
+
+---@type widget.checkbox
+local insensitive = CheckBox(widget, "Insensitive")
+insensitive:set_position(10, line_options:get_bottom() + 10)
+insensitive:set_tooltip("Case insensitive search")
+insensitive:set_checked(true)
+
+---@type widget.checkbox
+local patterncheck = CheckBox(widget, "Pattern")
+patterncheck:set_position(10, insensitive:get_bottom() + 10)
+patterncheck:set_tooltip("Treat search text as a lua pattern")
+
+---@type widget.checkbox
+local regexcheck = CheckBox(widget, "Regex")
+regexcheck:set_position(10, patterncheck:get_bottom() + 10)
+regexcheck:set_tooltip("Treat search text as a regular expression")
+
+---@type widget.checkbox
+local replaceinselection = CheckBox(widget, "Replace in Selection")
+replaceinselection:set_position(10, regexcheck:get_bottom() + 10)
+replaceinselection:set_tooltip("Perform replace only on selected text")
+
+---@type widget.selectbox
+local scope = SelectBox(widget, "scope")
+scope:set_position(10, regexcheck:get_bottom() + 10)
+scope:add_option("current file")
+scope:add_option("project files")
+scope:set_selected(1)
+
+---@type widget.filepicker
+local filepicker = FilePicker(widget)
+filepicker:set_mode(FilePicker.mode.DIRECTORY)
+filepicker:set_position(10, scope:get_bottom() + 10)
+filepicker:set_tooltip("Directory to perform the search")
+filepicker:hide()
+
+---@type widget.line
+local statusline = Line(widget)
+statusline:set_position(0, scope:get_bottom() + 10)
+
+---@type widget.label
+local status = Label(widget, "")
+status:set_position(10, statusline:get_bottom() + 10)
+
+--------------------------------------------------------------------------------
+-- Helper class to keep track of amount of matches and display on status label
+--------------------------------------------------------------------------------
+---@class plugins.search_ui.result
+---@field line integer
+---@field col integer
+
+---@class plugins.search_ui.results
+---@field text string
+---@field matches plugins.search_ui.result[]
+---@field doc core.doc?
+local Results = {
+ text = "",
+ matches = {},
+ doc = nil,
+ prev_search_id = 0
+}
+
+---@param text string
+---@param doc core.doc
+function Results:find(text, doc, force)
+ if self.text == text and self.doc == doc and not force then
+ self:set_status()
+ return
+ end
+
+ -- disable previous search thread
+ if self.prev_search_id > 0 and core.threads[self.prev_search_id] then
+ core.threads[self.prev_search_id] = {
+ cr = coroutine.create(function() end), wake = 0
+ }
+ end
+
+ self.text = text
+ self.doc = doc
+
+ local search_func
+
+ -- regex search
+ if regexcheck:is_checked() then
+ local regex_find_offsets = regex.match
+ if regex.find_offsets then
+ regex_find_offsets = regex.find_offsets
+ end
+ local pattern = regex.compile(
+ findtext:get_text(),
+ insensitive:is_checked() and "im" or "m"
+ )
+ if not pattern then return end
+ search_func = function(line_text)
+ ---@cast line_text string
+ local results = nil
+ local offsets = {regex_find_offsets(pattern, line_text)}
+ if offsets[1] then
+ results = {}
+ for i=1, #offsets, 2 do
+ table.insert(results, offsets[i])
+ end
+ end
+ return results
+ end
+ -- plain or pattern search
+ else
+ local no_case = insensitive:is_checked()
+ local is_plain = not patterncheck:is_checked()
+ if is_plain and no_case then
+ text = text:ulower()
+ end
+ search_func = function(line_text)
+ ---@cast line_text string
+ if is_plain and no_case then
+ line_text = line_text:ulower()
+ end
+ local results = nil
+ local col1, col2 = line_text:find(text, 1, is_plain)
+ if col1 then
+ results = {}
+ table.insert(results, col1)
+ while col1 do
+ col1, col2 = line_text:find(text, col2+1, is_plain)
+ if col1 then
+ table.insert(results, col1)
+ end
+ end
+ end
+ return results
+ end
+ end
+
+ self.prev_search_id = core.add_thread(function()
+ self.matches = {}
+ local lines_count = #doc.lines
+ for i=1, lines_count do
+ local offsets = search_func(doc.lines[i])
+ if offsets then
+ for _, col in ipairs(offsets) do
+ table.insert(self.matches, {line = i, col = col})
+ end
+ end
+ if i % 100 == 0 then
+ coroutine.yield()
+ end
+ end
+ self:set_status()
+ end)
+end
+
+---@return integer
+function Results:current()
+ if not self.doc then return 0 end
+ local line1, col1, line2, col2 = self.doc:get_selection()
+ if line1 == line2 and col1 == col2 then return 0 end
+ local line = math.min(line1, line2)
+ local col = math.min(col1, col2)
+ if self.matches and #self.matches > 0 then
+ for i, result in ipairs(self.matches) do
+ if result.line == line and result.col == col then
+ return i
+ end
+ end
+ end
+ return 0
+end
+
+function Results:clear()
+ self.text = ""
+ self.matches = {}
+ self.doc = nil
+ status:set_label("")
+end
+
+function Results:set_status()
+ local current = self:current()
+ local total = self.matches and #self.matches or 0
+ if total > 0 then
+ status:set_label(
+ "Result: " .. tostring(current .. " of " .. tostring(total))
+ )
+ else
+ status:set_label("")
+ end
+end
+
+--------------------------------------------------------------------------------
+-- Helper functions
+--------------------------------------------------------------------------------
+local function view_is_open(target_view)
+ if not target_view then return false end
+ local found = false
+ for _, view in ipairs(core.root_view.root_node:get_children()) do
+ if view == target_view then
+ found = true
+ break
+ end
+ end
+ return found
+end
+
+local function toggle_scope(idx, not_set)
+ if not not_set then scope:set_selected(idx) end
+
+ if idx == 1 then
+ replacetext:show()
+ findnext:show()
+ findprev:show()
+ replace:show()
+ patterncheck:show()
+ replaceinselection:show()
+ findproject:hide()
+ filepicker:hide()
+
+ if view_is_open(doc_view) and findtext:get_text() ~= "" then
+ Results:find(findtext:get_text(), doc_view.doc)
+ else
+ Results:clear()
+ end
+ else
+ replacetext:hide()
+ findnext:hide()
+ findprev:hide()
+ replace:hide()
+ patterncheck:hide()
+ replaceinselection:hide()
+ findproject:show()
+ filepicker:show()
+
+ Results:clear()
+ end
+end
+
+local function project_search()
+ if findtext:get_text() == "" then return end
+ if not regexcheck:is_checked() then
+ projectsearch.search_plain(
+ findtext:get_text(), filepicker:get_path(), insensitive:is_checked()
+ )
+ else
+ projectsearch.search_regex(
+ findtext:get_text(), filepicker:get_path(), insensitive:is_checked()
+ )
+ end
+ command.perform "search-replace:hide"
+end
+
+local find_enabled = true
+local function find(reverse)
+ if
+ not view_is_open(doc_view) or findtext:get_text() == "" or not find_enabled
+ then
+ Results:clear()
+ return
+ end
+
+ if core.last_active_view and core.last_active_view:is(DocView) then
+ doc_view = core.last_active_view
+ end
+
+ local doc = doc_view.doc
+ local cline1, ccol1, cline2, ccol2 = doc:get_selection()
+ local line, col = cline1, ccol1
+ if reverse and ccol2 < ccol1 then
+ col = ccol2
+ end
+
+ local opt = {
+ wrap = true,
+ no_case = insensitive:is_checked(),
+ pattern = patterncheck:is_checked(),
+ regex = regexcheck:is_checked(),
+ reverse = reverse
+ }
+
+ if opt.regex and not regex.compile(findtext:get_text()) then
+ return
+ end
+
+ status:set_label("")
+
+ core.try(function()
+ local line1, col1, line2, col2 = search.find(
+ doc, line, col, findtext:get_text(), opt
+ )
+
+ local current_text = doc:get_text(
+ table.unpack({ doc:get_selection() })
+ )
+
+ if opt.no_case and not opt.regex and not opt.pattern then
+ current_text = current_text:ulower()
+ end
+
+ if line1 then
+ local text = findtext:get_text()
+ if opt.no_case and not opt.regex and not opt.pattern then
+ text = text:ulower()
+ end
+ if reverse or (current_text == text or current_text == "") then
+ doc:set_selection(line1, col2, line2, col1)
+ else
+ doc:set_selection(line1, col1, line2, col2)
+ end
+ doc_view:scroll_to_line(line1, true)
+ Results:find(text, doc)
+ end
+ end)
+end
+
+local function find_replace()
+ if core.last_active_view:is(DocView) then
+ doc_view = core.last_active_view
+ end
+ local doc = doc_view.doc
+
+ if not replaceinselection:is_checked() then
+ local line1, col1, line2, col2 = doc:get_selection()
+ if line1 ~= line2 or col1 ~= col2 then
+ doc:set_selection(line1, col1)
+ end
+ end
+
+ local old = findtext:get_text()
+ local new = replacetext:get_text()
+
+ local results = doc:replace(function(text)
+ if not regexcheck:is_checked() then
+ if not patterncheck:is_checked() then
+ return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil)
+ else
+ return text:gsub(old, new)
+ end
+ end
+ local result, matches = regex.gsub(regex.compile(old, "m"), text, new)
+ if type(matches) == "table" then
+ return result, #matches
+ end
+ return result, matches
+ end)
+
+ local n = 0
+ for _,v in pairs(results) do
+ n = n + v
+ end
+
+ status:set_label(string.format("Total Replaced: %d", n))
+end
+
+local inside_node = false
+local current_node = nil
+local current_position = ""
+
+local function add_to_node()
+ if not inside_node or current_position ~= config.plugins.search_ui.position then
+ if
+ current_position ~= ""
+ and
+ current_position ~= config.plugins.search_ui.position
+ then
+ widget:hide()
+ current_node:remove_view(core.root_view.root_node, widget)
+ core.root_view.root_node:update_layout()
+ widget:set_size(0, 0)
+ widget.init_size = true
+ end
+ local node = core.root_view:get_primary_node()
+ if config.plugins.search_ui.position == "right" then
+ current_node = node:split("right", widget, {x=true}, true)
+ current_position = "right"
+ elseif config.plugins.search_ui.position == "top" then
+ current_node = node:split("up", widget, {y=true}, false)
+ current_position = "top"
+ else
+ current_node = node:split("down", widget, {y=true}, false)
+ current_position = "bottom"
+ end
+ widget:show()
+ inside_node = true
+ end
+end
+
+---Show or hide the search pane.
+---@param av? core.docview
+---@param toggle? boolean
+local function show_find(av, toggle)
+ if
+ not view_is_open(av)
+ and
+ scope:get_selected() == 1
+ then
+ widget:swap_active_child()
+ if config.plugins.search_ui.position == "right" then
+ widget:hide_animated(false, true)
+ else
+ widget:hide_animated(true, false)
+ end
+ return
+ end
+
+ if inside_node and current_position == config.plugins.search_ui.position then
+ if toggle then
+ widget:toggle_visible(true, false, true)
+ else
+ if not widget:is_visible() then
+ if config.plugins.search_ui.position == "right" then
+ widget:show_animated(false, true)
+ else
+ widget:show_animated(true, false)
+ end
+ end
+ end
+ else
+ add_to_node()
+ end
+
+ if widget:is_visible() then
+ status:set_label("")
+
+ widget:swap_active_child(findtext)
+ doc_view = av
+ if view_is_open(doc_view) and doc_view.doc then
+ local doc_text = doc_view.doc:get_text(
+ table.unpack({ doc_view.doc:get_selection() })
+ )
+ if insensitive:is_checked() then doc_text = doc_text:ulower() end
+ local current_text = findtext:get_text()
+ if insensitive:is_checked() then current_text = current_text:ulower() end
+ if doc_text and doc_text ~= "" and current_text ~= doc_text then
+ local original_text = doc_view.doc:get_text(
+ table.unpack({ doc_view.doc:get_selection() })
+ )
+ find_enabled = false
+ findtext:set_text(original_text)
+ find_enabled = true
+ elseif current_text ~= "" and doc_text == "" then
+ if scope:get_selected() == 1 then
+ find(false)
+ end
+ end
+ if findtext:get_text() ~= "" then
+ findtext.textview.doc:set_selection(1, math.huge, 1, 1)
+ if scope:get_selected() == 1 then
+ Results:find(findtext:get_text(), doc_view.doc)
+ else
+ Results:clear()
+ end
+ else
+ Results:clear()
+ end
+ end
+ else
+ widget:swap_active_child()
+ if view_is_open(doc_view) then
+ core.set_active_view(doc_view)
+ end
+ end
+end
+
+--------------------------------------------------------------------------------
+-- Widgets event overrides
+--------------------------------------------------------------------------------
+function findtext:on_change(text)
+ if scope:get_selected() == 1 then
+ find(false)
+ end
+end
+
+function insensitive:on_checked(checked)
+ Results:clear()
+end
+
+function patterncheck:on_checked(checked)
+ if checked then
+ regexcheck:set_checked(false)
+ end
+ Results:clear()
+end
+
+function regexcheck:on_checked(checked)
+ if checked then
+ patterncheck:set_checked(false)
+ end
+ Results:clear()
+end
+
+function scope:on_selected(idx)
+ toggle_scope(idx, true)
+ if not view_is_open(doc_view) and idx == 1 then
+ command.perform "search-replace:hide"
+ end
+end
+
+function findnext:on_click() find(false) end
+function findprev:on_click() find(true) end
+function findproject:on_click() project_search() end
+function replace:on_click() find_replace() end
+
+---@param self widget
+local function update_size(self)
+ if config.plugins.search_ui.position == "right" then
+ if scope:get_selected() == 1 then
+ if self.size.x < replace:get_right() + replace:get_width() / 2 then
+ self.size.x = replace:get_right() + replace:get_width() / 2
+ end
+ else
+ if self.size.x < findproject:get_right() + findproject:get_width() * 2 then
+ self.size.x = findproject:get_right() + findproject:get_width() * 2
+ end
+ end
+ else
+ self:set_size(nil, self:get_real_height() + 10)
+ end
+end
+
+---@param self widget
+local function update_right_positioning(self)
+ scope:show()
+ label:show()
+ status:show()
+ line_options:show()
+ label:set_label("Find and Replace")
+
+ label:set_position(10, 10)
+ line:set_position(0, label:get_bottom() + 10)
+ findtext:set_position(10, line:get_bottom() + 10)
+ findtext.size.x = self.size.x - 20
+
+ if scope:get_selected() == 1 then
+ replacetext:set_position(10, findtext:get_bottom() + 10)
+ replacetext.size.x = self.size.x - 20
+ findprev:set_position(10, replacetext:get_bottom() + 10)
+ findnext:set_position(findprev:get_right() + 5, replacetext:get_bottom() + 10)
+ replace:set_position(findnext:get_right() + 5, replacetext:get_bottom() + 10)
+ line_options:set_position(0, replace:get_bottom() + 10)
+ else
+ findproject:set_position(10, findtext:get_bottom() + 10)
+ replace:set_position(findproject:get_right() + 5, replacetext:get_bottom() + 10)
+ line_options:set_position(0, findproject:get_bottom() + 10)
+ end
+
+ insensitive:set_position(10, line_options:get_bottom() + 10)
+ if scope:get_selected() == 1 then
+ patterncheck:set_position(10, insensitive:get_bottom() + 10)
+ regexcheck:set_position(10, patterncheck:get_bottom() + 10)
+ replaceinselection:set_position(10, regexcheck:get_bottom() + 10)
+ scope:set_position(10, replaceinselection:get_bottom() + 10)
+ else
+ regexcheck:set_position(10, insensitive:get_bottom() + 10)
+ scope:set_position(10, regexcheck:get_bottom() + 10)
+ end
+
+ scope:set_size(self.size.x - 20)
+ if scope:get_selected() == 1 then
+ statusline:set_position(0, scope:get_bottom() + 30)
+ else
+ filepicker:set_position(10, scope:get_bottom() + 10)
+ filepicker:set_size(self.size.x - 20, nil)
+ statusline:set_position(0, filepicker:get_bottom() + 30)
+ end
+
+ status:set_position(10, statusline:get_bottom() + 10)
+ if status.label == "" then
+ statusline:hide()
+ else
+ statusline:show()
+ end
+
+ if self.init_size then
+ update_size(self)
+ self.init_size = false
+ self:show_animated(false, true)
+ end
+
+ add_to_node()
+end
+
+---@param self widget
+local function update_bottom_positioning(self)
+ scope:hide()
+ statusline:hide()
+
+ if scope:get_selected() == 1 then
+ label:hide()
+ status:show()
+ status:set_position(10, 10)
+ replaceinselection:set_position(self.size.x - replaceinselection:get_width() - 10, 10)
+ regexcheck:set_position(replaceinselection:get_position().x - 10 - regexcheck:get_width(), 10)
+ patterncheck:set_position(regexcheck:get_position().x - 10 - patterncheck:get_width(), 10)
+ insensitive:set_position(patterncheck:get_position().x - 10 - insensitive:get_width(), 10)
+ line:set_position(0, status:get_bottom() + 10)
+ else
+ label:show()
+ status:hide()
+ label:set_label("Find in Directory")
+ label:set_position(10, 10)
+ regexcheck:set_position(self.size.x - regexcheck:get_width() - 10, 10)
+ insensitive:set_position(regexcheck:get_position().x - 10 - insensitive:get_width(), 10)
+ line:set_position(0, label:get_bottom() + 10)
+ end
+
+ if scope:get_selected() == 1 then
+ findtext:set_position(10, line:get_bottom() + 10)
+ findtext.size.x = self.size.x - 40 - findprev:get_width() - findnext:get_width()
+ findnext:set_position(self.size.x - 10 - findnext:get_width(), line:get_bottom() + 10)
+ findprev:set_position(findnext:get_position().x - 10 - findprev:get_width(), line:get_bottom() + 10)
+ replacetext:set_position(10, findtext:get_bottom() + 10)
+ replacetext.size.x = findtext.size.x
+ replace:set_position(self.size.x - 15 - replace:get_width(), findtext:get_bottom() + 10)
+ replace.size.x = findprev:get_width() + findnext:get_width() + 10
+ line_options:hide()
+ else
+ findtext:set_position(10, line:get_bottom() + 10)
+ findtext.size.x = self.size.x - 30 - findproject:get_width()
+ findproject:set_position(self.size.x - 10 - findproject:get_width(), line:get_bottom() + 10)
+ replace:set_position(findproject:get_right() + 5, replacetext:get_bottom() + 10)
+ line_options:show()
+ line_options:set_position(0, findproject:get_bottom() + 10)
+ filepicker:set_position(10, line_options:get_bottom() + 10)
+ filepicker:set_size(self.size.x - 20, nil)
+ end
+
+ if self.init_size then
+ update_size(self)
+ self.init_size = false
+ self:show_animated(true, false)
+ end
+
+ add_to_node()
+end
+
+-- reposition items on scale changes
+function widget:update()
+ if Widget.update(self) then
+ if config.plugins.search_ui.position == "right" then
+ update_right_positioning(self)
+ else
+ update_bottom_positioning(self)
+ end
+ end
+end
+
+function widget:on_scale_change(...)
+ Widget.on_scale_change(self, ...)
+ update_size(self)
+end
+
+--------------------------------------------------------------------------------
+-- Override set_active_view to keep track of currently active docview
+--------------------------------------------------------------------------------
+local core_set_active_view = core.set_active_view
+function core.set_active_view(...)
+ core_set_active_view(...)
+ local view = core.next_active_view or core.active_view
+ if
+ view ~= doc_view
+ and
+ widget:is_visible()
+ and
+ view:extends(DocView)
+ and
+ view ~= findtext.textview
+ and
+ view ~= replacetext.textview
+ and
+ view.doc.filename
+ then
+ doc_view = view
+ Results:clear()
+ end
+end
+
+--------------------------------------------------------------------------------
+-- Register commands
+--------------------------------------------------------------------------------
+command.add(
+ function()
+ if core.active_view:is(DocView) then
+ return true, core.active_view
+ elseif widget:is_visible() then
+ return true, doc_view
+ elseif scope:get_selected() == 2 then
+ return true, nil
+ end
+ return false
+ end,
+ {
+ ["search-replace:show"] = function(av)
+ show_find(av, false)
+ end,
+
+ ["search-replace:toggle"] = function(av)
+ show_find(av, true)
+ end
+ }
+)
+
+command.add(function() return widget:is_visible() and not core.active_view:is(CommandView) end, {
+ ["search-replace:hide"] = function()
+ widget:swap_active_child()
+ if config.plugins.search_ui.position == "right" then
+ widget:hide_animated(false, true)
+ else
+ widget:hide_animated(true, false)
+ end
+ if view_is_open(doc_view) then
+ core.set_active_view(doc_view)
+ end
+ end,
+
+ ["search-replace:file-search"] = function()
+ toggle_scope(1)
+ command.perform "search-replace:show"
+ end,
+
+ ["search-replace:next"] = function()
+ find(false)
+ end,
+
+ ["search-replace:previous"] = function()
+ find(true)
+ end,
+
+ ["search-replace:toggle-sensitivity"] = function()
+ insensitive:set_checked(not insensitive:is_checked())
+ Results:clear()
+ end,
+
+ ["search-replace:toggle-regex"] = function()
+ regexcheck:set_checked(not regexcheck:is_checked())
+ Results:clear()
+ end,
+
+ ["search-replace:toggle-in-selection"] = function()
+ replaceinselection:set_checked(not replaceinselection:is_checked())
+ end
+})
+
+command.add(
+ function()
+ return widget:is_visible()
+ and
+ not core.active_view:is(CommandView)
+ and
+ (
+ widget.child_active == findtext
+ or
+ widget.child_active == replacetext
+ )
+ end,
+ {
+ ["search-replace:perform"] = function()
+ if scope:get_selected() == 1 then
+ if widget.child_active == findtext then
+ ---@type core.doc
+ local doc = doc_view.doc
+ local line1, col1, line2, col2 = doc:get_selection()
+ -- correct cursor position to properly search next result
+ if line1 ~= line2 or col1 ~= col2 then
+ doc:set_selection(
+ line1,
+ math.max(col1, col2),
+ line2,
+ math.min(col1, col2)
+ )
+ end
+ find(false)
+ else
+ find_replace()
+ end
+ else
+ project_search()
+ end
+ end
+ }
+)
+
+--------------------------------------------------------------------------------
+-- Override core find/replace commands
+--------------------------------------------------------------------------------
+local find_replace_find = command.map["find-replace:find"].perform
+command.map["find-replace:find"].perform = function(...)
+ if config.plugins.search_ui.replace_core_find then
+ toggle_scope(1)
+ command.perform "search-replace:show"
+ else
+ find_replace_find(...)
+ end
+end
+
+local find_replace_replace = command.map["find-replace:replace"].perform
+command.map["find-replace:replace"].perform = function(...)
+ if config.plugins.search_ui.replace_core_find then
+ toggle_scope(1)
+ command.perform "search-replace:show"
+ else
+ find_replace_replace(...)
+ end
+end
+
+local find_replace_repeat = command.map["find-replace:repeat-find"].perform
+command.map["find-replace:repeat-find"].perform = function(...)
+ if
+ widget:is_visible()
+ or
+ (config.plugins.search_ui.replace_core_find and findtext:get_text() ~= "")
+ then
+ find(false)
+ return
+ end
+ find_replace_repeat(...)
+end
+
+local find_replace_previous = command.map["find-replace:previous-find"].perform
+command.map["find-replace:previous-find"].perform = function(...)
+ if
+ widget:is_visible()
+ or
+ (config.plugins.search_ui.replace_core_find and findtext:get_text() ~= "")
+ then
+ find(true)
+ return
+ end
+ find_replace_previous(...)
+end
+
+local project_search_find = command.map["project-search:find"].perform
+command.map["project-search:find"].perform = function(path)
+ if config.plugins.search_ui.replace_core_find then
+ toggle_scope(2)
+ if path then
+ filepicker:set_path(path)
+ end
+ local av = doc_view
+ if
+ core.active_view:extends(DocView)
+ and
+ core.active_view ~= findtext.textview
+ and
+ core.active_view ~= replacetext.textview
+ then
+ av = core.active_view
+ end
+ show_find(av, false)
+ return
+ end
+ project_search_find(path)
+end
+
+--------------------------------------------------------------------------------
+-- Register keymaps
+--------------------------------------------------------------------------------
+keymap.add {
+ ["alt+h"] = "search-replace:toggle",
+ ["escape"] = "search-replace:hide",
+ ["f3"] = "search-replace:next",
+ ["shift+f3"] = "search-replace:previous",
+ ["return"] = "search-replace:perform",
+ ["shift+return"] = "search-replace:previous",
+ ["ctrl+i"] = "search-replace:toggle-sensitivity",
+ ["ctrl+shift+i"] = "search-replace:toggle-regex",
+ ["ctrl+alt+i"] = "search-replace:toggle-in-selection",
+ ["ctrl+f"] = "search-replace:file-search"
+}
+
+
+return widget