aboutsummaryrefslogtreecommitdiff
-- 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