-- Copyright 2011-18 Paul Kulchenko, ZeroBrane LLC
-- authors: Lomtik Software (J. Winwood & John Labenski)
-- Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------

local ide = ide
local searchpanel = 'searchpanel'
local q = EscapeMagic
local unpack = table.unpack or unpack
ide.findReplace = {
  panel = nil, -- the control for find/replace
  replace = false, -- is it a find or replace
  infiles = false,
  backfocus = nil, -- editor and position to return focus to
  cureditor = nil, -- the editor being searched
  reseditor = nil, -- the editor for search results
  oveditor = nil, -- the editor is used for search during find-in-files
  findCtrl = nil, -- the control that has the search text
  replaceCtrl = nil, -- the control that has the replace text
  scopeText = nil,
  foundString = false, -- was the string found for the last search
  curfilename = "", -- for search in files
  inselection = false,
  occurrences = 0,
  files = 0,

  settings = {
    flags = {
      WholeWord = false, -- match whole words
      MatchCase = false, -- case sensitive
      RegularExpr = false, -- use regex
      Wrap = true, -- search wraps around
      Down = true, -- search downwards in doc
      Context = true, -- include context in search results
      SubDirs = true, -- search in subdirectories
      FollowSymlink = false, -- search symlink sub-directories
      MapDirs = false, -- search in mapped directories
      MultiResults = false, -- show multiple result tabs
    },
    flist = {},
    rlist = {},
    slist = {},
  },

  -- HasText() is there a string to search for
  -- GetSelection() get currently selected string if it's on one line
  -- Find(reverse) find the text
  -- Show(replace) create the dialog
  -- GetEditor() which editor to use
}
local findReplace = ide.findReplace
local replaceHintText = '<replace with>'
local sep = ';'

function findReplace:GetEditor(reset)
  if reset or not ide:IsValidCtrl(self.cureditor) then self.cureditor = nil end
  self.cureditor = ide:GetEditorWithLastFocus() or self.cureditor
  return self.oveditor or self.cureditor or ide:GetEditor()
end

-------------------- Find replace dialog

local function setSearchFlags(editor)
  local flags = wxstc.wxSTC_FIND_POSIX
  local f = findReplace.settings.flags
  if f.WholeWord then flags = flags + wxstc.wxSTC_FIND_WHOLEWORD end
  if f.MatchCase then flags = flags + wxstc.wxSTC_FIND_MATCHCASE end
  if f.RegularExpr then flags = flags + wxstc.wxSTC_FIND_REGEXP end
  editor:SetSearchFlags(flags)
end

local function setTarget(editor, flags)
  flags = flags or {}
  local fDown, fAll, fWrap = flags.Down, flags.All, flags.Wrap
  local len = editor:GetLength()
  local selStart, selEnd = editor:GetSelectionStart(), editor:GetSelectionEnd()
  local s, e
  if fDown then
    e = flags.EndPos or len
    s = math.min(e, math.max(flags.StartPos or 0, (fAll and selStart or selEnd)))
  else -- reverse the range for the backward search
    e = flags.StartPos or 0
    s = math.max(e, math.min(flags.EndPos or len, (fAll and selEnd or selStart)))
  end
  -- if wrap around and search all requested, then search the entire document
  if fAll and fWrap then s, e = 0, len end
  editor:SetTargetStart(s)
  editor:SetTargetEnd(e)
  return e
end

function findReplace:IsPreview(editor)
  local ok, ispreview = pcall(function() return editor and editor.searchpreview end)
  return ok and ispreview and true or false
end

function findReplace:CanSave(editor)
  return editor and editor:GetModify() and self:IsPreview(editor) and editor or nil
end

function findReplace:HasText()
  if not self.panel then self:createPanel() end
  local findText = self.findCtrl:GetValue()
  return findText ~= nil and #findText > 0 and findText or nil
end

function findReplace:SetStatus(msg)
  if self.status then self.status:SetLabel(msg) end
end

function findReplace:SetFind(text)
  if not self.panel then self:createPanel() end
  local ctrl = self.findCtrl
  if text and ctrl then
    if ctrl:GetValue() ~= text then ctrl:ChangeValue(text) end
    return text
  end
  return
end

function findReplace:GetFind(...) return self:HasText() end

function findReplace:GetFlags() return self.settings.flags end

function findReplace:SetReplace(text)
  if not self.panel then self:createPanel() end
  local ctrl = self.replaceCtrl
  if text and ctrl then
    if ctrl:GetValue() ~= text then ctrl:ChangeValue(text) end
    return text
  end
  return
end

function findReplace:GetScope()
  local scopeval = self.scope:GetValue()
  local dir, mask = scopeval:match(('([^%s]*)%s%%s*(.+)'):format(sep,sep))
  if not dir then dir = scopeval end
  -- strip leading/trailing spaces from the directory
  dir = dir:gsub("^%s+",""):gsub("%s+$","")
  -- if the directory doesn't exist, treat it as the extension(s)
  if not mask and not wx.wxDirExists(dir) and dir:find('%*') then
    dir, mask = ide:GetProject() or wx.wxGetCwd(), (#dir > 0 and dir or nil)
  end
  return dir, mask
end

function findReplace:SetScope(dir, mask)
  return dir .. (mask and (sep..' '..mask) or "")
end

function findReplace:GetScopeMRU(head)
  local patt, match = "^"..q(head)
  for _, v in ipairs(findReplace.settings.slist) do
    if v:find(patt) then match = v; break end
  end
  return match
end

function findReplace:GetWordAtCaret()
  local editor = self:GetEditor()
  if editor then
    local pos = editor:GetCurrentPos()
    local text = editor:GetTextRangeDyn( -- try to select a word under caret
      editor:WordStartPosition(pos, true), editor:WordEndPosition(pos, true))
    if #text == 0 then
      editor:GetTextRangeDyn( -- try to select a non-word under caret
        editor:WordStartPosition(pos, false), editor:WordEndPosition(pos, false))
    end
    return #text > 0 and text or nil
  end
  return
end

function findReplace:GetSelection()
  local editor = self:GetEditor()
  if editor then
    local startSel = editor:GetSelectionStart()
    local endSel = editor:GetSelectionEnd()
    if (startSel ~= endSel)
    and (editor:LineFromPosition(startSel) == editor:LineFromPosition(endSel)) then
      return editor:GetTextRangeDyn(startSel, endSel)
    end
  end
  return
end

function findReplace:Find(reverse)
  if not self.panel then self:createPanel() end
  local findText = self.findCtrl:GetValue()

  local msg = ""
  local editor = self:GetEditor()
  if editor and self:HasText() then
    local fDown = (reverse and not self:GetFlags().Down) or (not reverse and self:GetFlags().Down)
    local bf = self.inselection and self.backfocus or {}
    setSearchFlags(editor)
    setTarget(editor, {Down = fDown, StartPos = bf.spos, EndPos = bf.epos})
    local posFind = editor:SearchInTarget(findText)
    if (posFind == wx.wxNOT_FOUND) and self:GetFlags().Wrap then
      editor:SetTargetStart(fDown and (bf.spos or 0) or (bf.epos or editor:GetLength()))
      editor:SetTargetEnd(fDown and (bf.epos or editor:GetLength()) or (bf.spos or 0))
      posFind = editor:SearchInTarget(findText)
      msg = (self.inselection
        and TR("Reached end of selection and wrapped around.")
        or TR("Reached end of text and wrapped around.")
      )
    end
    if posFind == wx.wxNOT_FOUND then
      self.foundString = false
      msg = TR("Text not found.")
      local parent = editor:GetParent()
      if parent and parent:GetClassInfo():GetClassName() == 'wxAuiNotebook' then
        local nb = parent:DynamicCast("wxAuiNotebook")
        local index = nb:GetPageIndex(editor)
        if index ~= wx.wxNOT_FOUND then msg = nb:GetPageText(index)..": "..msg end
      end
    else
      self.foundString = true
      local start = editor:GetTargetStart()
      local finish = editor:GetTargetEnd()
      editor:SetSelection(start, finish)
      editor:ShowRange(finish, start)
    end
  end
  self:SetStatus(msg)
  return self.foundString
end

-- returns true if something was found
-- [inFileRegister(pos)] passing function will
-- register every position item was found

function findReplace:FindAll(inFileRegister)
  if not self.panel then self:createPanel() end
  local findText = self.findCtrl:GetValue()

  local found = false
  local editor = self:GetEditor()
  if editor and self:HasText() then
    local e = setTarget(editor, {All = true, Wrap = true})

    setSearchFlags(editor)
    while true do
      local posFind = editor:SearchInTarget(findText)
      if posFind == wx.wxNOT_FOUND then break end
      inFileRegister(posFind, editor:GetTargetEnd()-posFind)
      editor:SetTargetStart(editor:GetTargetEnd())
      editor:SetTargetEnd(e)
      found = true
    end
    if inFileRegister and found then inFileRegister() end
  end

  return found
end

local indicator = {
  SEARCHMATCH = ide:GetIndicator("core.searchmatch"),
  SEARCHSELECTION = ide:GetIndicator("core.searchselection"),
}

-- returns true if replacements were done
function findReplace:Replace(fReplaceAll, resultsEditor)
  if not self.panel then self:createPanel() end

  local findText = self.findCtrl:GetValue()
  local replaceText = self.replaceCtrl:GetValue()
  if replaceText == replaceHintText then replaceText = "" end

  local replaced = false
  local editor = resultsEditor or self:GetEditor()
  if editor and self:HasText() then
    -- don't replace in read-only editors
    if editor:GetReadOnly() then
      self:SetStatus(TR("Can't replace in read-only text."))
      return false
    end

    -- in the preview results always replace in the entire file
    local bf = self.inselection and self.backfocus
    local endTarget = (resultsEditor and setTarget(editor, {All = true, Wrap = true})
      -- when selection is marked, only replace in the selection
      or (bf and setTarget(editor, {Down = self:GetFlags().Down, All = fReplaceAll, StartPos = bf.spos, EndPos = bf.epos}))
      -- in all other cases, replace as selected
      or setTarget(editor, {Down = self:GetFlags().Down, All = fReplaceAll, Wrap = self:GetFlags().Wrap})
    )

    if fReplaceAll then
      if resultsEditor then editor:SetIndicatorCurrent(indicator.SEARCHMATCH) end

      setSearchFlags(editor)
      local occurrences = 0
      local posFind = editor:SearchInTarget(findText)
      if posFind ~= wx.wxNOT_FOUND then
        editor:BeginUndoAction()
        while posFind ~= wx.wxNOT_FOUND do
          local length = editor:GetLength()
          -- if replace-in-files (resultsEditor) is being done,
          -- then check that the match starts with %d+:
          local match = true
          if resultsEditor then
            local line = editor:LineFromPosition(posFind)
            local _, _, prefix = editor:GetLineDyn(line):find("^(%s*%d+: )")
            match = prefix and posFind >= editor:PositionFromLine(line)+#prefix
          end
          if match then
            local replaced = self:GetFlags().RegularExpr
              and editor:ReplaceTargetRE(replaceText)
              or editor:ReplaceTarget(replaceText)

            -- mark replaced text
            if resultsEditor then editor:IndicatorFillRange(posFind, replaced) end
            occurrences = occurrences + 1
          end

          editor:SetTargetStart(editor:GetTargetEnd())
          -- adjust the endTarget as the position could have changed;
          -- can't simply subtract text length as it could be a regexp
          local adjusted = editor:GetLength() - length
          endTarget = endTarget + adjusted
          -- also adjust the selection as the end marker can move after replacement
          if bf and bf.epos then bf.epos = bf.epos + adjusted end
          editor:SetTargetEnd(endTarget)
          posFind = editor:SearchInTarget(findText)
        end
        editor:EndUndoAction()
        replaced = true
      end
      self:SetStatus(
        TR("Replaced %d instance.", occurrences):format(occurrences))
    else
      editor:TargetFromSelection()
      -- check if there is anything selected as well as the user can
      -- move the cursor after successful search
      if editor:GetSelectionStart() ~= editor:GetSelectionEnd()
      -- check that the current selection matches what's being searched for
      and editor:SearchInTarget(findText) ~= wx.wxNOT_FOUND then
        local length = editor:GetLength()
        local start = editor:GetSelectionStart()
        local replaced = self:GetFlags().RegularExpr
          and editor:ReplaceTargetRE(replaceText)
          or editor:ReplaceTarget(replaceText)
        local adjusted = editor:GetLength() - length
        if bf and bf.epos then bf.epos = bf.epos + adjusted end

        editor:SetSelection(start, start + replaced)
        self.foundString = false

        replaced = true
      end
      self:Find()
    end
  end

  return replaced
end

local oldline
local FILE_MARKER = ide:GetMarker("searchmatchfile")
local FILE_MARKER_VALUE = 2^FILE_MARKER
local function getRawLine(ed, line) return (ed:GetLineDyn(line):gsub("[\n\r]+$","")) end
local function onFileRegister(pos, length)
  local editor = findReplace.oveditor
  local reseditor = findReplace.reseditor
  local posline = pos and editor:LineFromPosition(pos) + 1
  local text = ""
  local cfg = ide.config.search
  local contextb = findReplace:GetFlags().Context and cfg.contextlinesbefore or 0
  local contexta = findReplace:GetFlags().Context and cfg.contextlinesafter or 0
  local lines = reseditor:GetLineCount() -- current number of lines

  -- check if there is another match on the same line; do not add anything
  if oldline ~= posline then
    if posline and not oldline then
      -- show file name and a bookmark marker
      reseditor:AppendTextDyn(findReplace.curfilename.."\n")
      reseditor:MarkerAdd(lines-1, FILE_MARKER)
      reseditor:SetFoldLevel(lines-1, reseditor:GetFoldLevel(lines-1)
        + wxstc.wxSTC_FOLDLEVELHEADERFLAG)
      findReplace:SetStatus(TR("Found match in '%s'."):format(GetFileName(findReplace.curfilename)))

      lines = lines + 1

      -- show context lines before posline
      for line = math.max(1, posline-contextb), posline-1 do
        text = text .. ("%5d  %s\n"):format(line, getRawLine(editor, line-1))
      end
    end
    if posline and oldline then
      -- show context lines between oldposline and posline
      for line = oldline+1, math.min(posline-1, oldline+contexta) do
        text = text .. ("%5d  %s\n"):format(line, getRawLine(editor, line-1))
      end
      if contextb + contexta > 0 and posline-oldline > contextb + contexta + 1 then
        text = text .. ("%5s\n"):format(("."):rep(#tostring(posline)))
      end
      for line = math.max(oldline+contexta+1, posline-contextb), posline-1 do
        text = text .. ("%5d  %s\n"):format(line, getRawLine(editor, line-1))
      end
    end
    if posline then
      text = text .. ("%5d: %s\n"):format(posline, getRawLine(editor, posline-1))
      findReplace.lines = findReplace.lines + 1
    elseif oldline then
      -- show context lines after posline
      for line = oldline+1, math.min(editor:GetLineCount(), oldline+contexta) do
        text = text .. ("%5d  %s\n"):format(line, getRawLine(editor, line-1))
      end
      text = text .. "\n"
    end
    oldline = posline

    reseditor:AppendTextDyn(text)

    for line = lines-1, reseditor:GetLineCount()-2 do
      reseditor:SetFoldLevel(line, wxstc.wxSTC_FOLDLEVELBASE + 1)
    end
  end

  if posline then
    findReplace.occurrences = findReplace.occurrences + 1

    -- get the added line
    local markline = reseditor:GetLineCount()-2
    -- get the match position in the file relative to the beginning of the line
    local localpos = pos - editor:PositionFromLine(posline-1)
    -- recalculate position in the search results relative to the line
    local newpos = reseditor:PositionFromLine(markline)+localpos+7 -- add indent
    reseditor:SetIndicatorCurrent(indicator.SEARCHMATCH)
    reseditor:IndicatorFillRange(newpos, length)
  end
end

local firstReadSize = 2048
local knownBinary = {}
local function checkBinary(ext, content)
  if not content then return knownBinary[ext] end
  if ext == "" then return IsBinary(content) end
  if knownBinary[ext] == nil then
    knownBinary[ext] = #ide:GetKnownExtensions(ext) == 0 and IsBinary(content)
  end
  return knownBinary[ext]
end

function findReplace:ProcInFiles(startdir,mask,subdirs)
  if not self.panel then self:createPanel() end

  local text = not self:GetFlags().RegularExpr and q(self.findCtrl:GetValue()) or nil
  if text and not self:GetFlags().MatchCase then
    text = text:gsub("%w",function(s) return "["..s:lower()..s:upper().."]" end)
  end

  local lastupdate = os.clock()
  local function yield(dir, force)
    local now = os.clock()
    if now-lastupdate < 0.1 and not force then return true end -- skip too frequent requests
    lastupdate = now

    if dir then self:SetStatus(TR("Searching in '%s'."):format(dir:gsub("^"..q(startdir),""))) end

    ide:Yield() -- give time to the UI to refresh
    -- the IDE may be quitting after Yield or the tab may be closed,
    local ok, mgr = pcall(function() return ide:GetUIManager() end)
    -- so check to make sure the manager is still active
    -- and check that the search results tab is still open
    -- return `nil` to abort processing if one of the checks fails
    return ok and mgr:GetPane(searchpanel):IsShown() and ide:IsValidCtrl(self.reseditor) or nil
  end

  local files = coroutine.wrap(function()
      ide:GetFileList(startdir, subdirs, mask, {
          yield = true, folder = false, skipbinary = true, ondirectory = yield,
          followsymlink = self:GetFlags().FollowSymlink,
        })
      -- also search mapped dirs if configured
      if self:GetFlags().MapDirs then
        local tree = ide:GetProjectTree()
        local item = tree:GetFirstChild(tree:GetRootItem())
        while item:IsOk() and tree:IsDirMapped(item) do
          ide:GetFileList(tree:GetItemFullName(item), subdirs, mask, {
              yield = true, folder = false, skipbinary = true, ondirectory = yield,
              followsymlink = self:GetFlags().FollowSymlink,
            })
          item = tree:GetNextSibling(item)
        end
      end
    end)
  while true do
    local file = files()
    if file == false then return false end -- aborted processing
    if file == nil then break end -- out of files to process

    if checkBinary(GetFileExt(file)) ~= true then
      self.curfilename = file
      local filetext, err = FileRead(file, firstReadSize)
      if not filetext then
        ide:Print(TR("Can't open file '%s': %s"):format(file, err))
      elseif not checkBinary(GetFileExt(file), filetext) then
        -- read the rest if there is more to read in the file
        if #filetext == firstReadSize then filetext = FileRead(file) end
        if filetext and (not text or filetext:find(text)) then
          self.oveditor:SetTextDyn(filetext)

          if self:FindAll(onFileRegister) then self.files = self.files + 1 end
          -- force a UI update on a match and abort if the user canceled the search
          if not yield(nil, true) then return false end
        end
      end
    end
  end
  return true
end

local function makePlural(word, counter) return word..(counter == 1 and '' or 's') end

function findReplace:RunInFiles(replace)
  if not self.panel then self:createPanel() end
  if not self:HasText() or self.oveditor then return end

  self.oveditor = ide:CreateStyledTextCtrl(self.panel, wx.wxID_ANY,
    wx.wxDefaultPosition, wx.wxSize(0,0), wx.wxBORDER_NONE)
  self.occurrences = 0
  self.lines = 0
  self.files = 0
  self.toolbar:UpdateWindowUI(wx.wxUPDATE_UI_FROMIDLE)
  ide:Yield() -- let the update of the UI happen

  -- save focus to restore after adding a page with search results
  local findText = self.findCtrl:GetValue()
  local flags = self:GetFlags()
  local showaseditor = ide.config.search.showaseditor
  local nb = ide:GetOutputNotebook()
  local reseditor = self.reseditor
  local resultsText = "Search Results"
  local previewText = resultsText..": "
  local valid = self:IsPreview(reseditor)
  -- open new tab if the current one is not valid
  -- or if multiple tabs are requested, but when searching for different text
  if not valid or (flags.MultiResults and reseditor.searchpreview ~= findText) then
    -- enable folds in the preview even if disabled in the editor
    local fold = ide.config.editor.fold
    ide.config.editor.fold = true
    if showaseditor then
      reseditor = NewFile(resultsText)
    else
      reseditor = ide:CreateBareEditor()
      reseditor:SetupKeywords("")

      local modpref = ide.MODPREF
      local function setModified(modified)
        local index = nb:GetPageIndex(reseditor)
        local text = nb:GetPageText(index):gsub("^"..q(modpref), "")
        nb:SetPageText(index, (modified and modpref or '')..text)
      end
      reseditor:Connect(wxstc.wxEVT_STC_SAVEPOINTREACHED,
        function () setModified(false) end)
      reseditor:Connect(wxstc.wxEVT_STC_SAVEPOINTLEFT,
        function () setModified(true) end)
      reseditor:Connect(wxstc.wxEVT_STC_MARGINCLICK,
        function (event)
          local editor = event:GetEventObject():DynamicCast('wxStyledTextCtrl')
          local line = editor:LineFromPosition(event:GetPosition())
          local header = bit.band(editor:GetFoldLevel(line),
            wxstc.wxSTC_FOLDLEVELHEADERFLAG) == wxstc.wxSTC_FOLDLEVELHEADERFLAG
          local shift, ctrl = wx.wxGetKeyState(wx.WXK_SHIFT), wx.wxGetKeyState(wx.WXK_CONTROL)
          if shift and ctrl then
            editor:FoldSome(line)
          elseif ctrl then -- select the scope that was clicked on
            local from = header and line or editor:GetFoldParent(line)
            if from > -1 then -- only select if there is a block to select
              local to = editor:GetLastChild(from, -1)
              editor:SetSelection(editor:PositionFromLine(from), editor:PositionFromLine(to+1))
            end
          elseif header or shift then
            editor:ToggleFold(line)
          end
        end)
      reseditor:Connect(wx.wxEVT_KEY_DOWN,
        function (event)
          if event:GetKeyCode() == wx.WXK_ESCAPE and self:IsShown() then
            self:Hide()
          else
            event:Skip()
          end
        end)

      -- mark as searchpreview to allow AddPage to add "close" button
      reseditor.searchpreview = findText
      local bmp = ide:GetBitmap("OUTPUT", "FIND", wx.wxSize(16, 16))
      nb:AddPage(reseditor, previewText, true, bmp)
    end
    reseditor:SetWrapMode(wxstc.wxSTC_WRAP_NONE)
    reseditor:SetIndentationGuides(false)
    if tonumber(ide.config.search.zoom) then
      reseditor:SetZoom(tonumber(ide.config.search.zoom))
    end
    for m = 0, ide.MAXMARGIN do -- hide all margins except folding
      if reseditor:GetMarginWidth(m) > 0
      and reseditor:GetMarginMask(m) ~= wxstc.wxSTC_MASK_FOLDERS then
        reseditor:SetMarginWidth(m, 0)
      end
    end
    reseditor:MarkerDefine(ide:GetMarker("searchmatchfile"))
    reseditor:Connect(wx.wxEVT_LEFT_DCLICK, function(event)
        if not wx.wxGetKeyState(wx.WXK_SHIFT)
        and not wx.wxGetKeyState(wx.WXK_CONTROL)
        and not wx.wxGetKeyState(wx.WXK_ALT) then
          local point = event:GetPosition()
          if point:GetX() <= reseditor:GetAllMarginWidth() then return end

          local pos = reseditor:PositionFromPoint(point)
          local line = reseditor:LineFromPosition(pos)
          local text = reseditor:GetLineDyn(line):gsub("[\n\r]+$","")
          -- get line with the line number
          local jumpline = text:match("^%s*(%d+)")
          local file
          if jumpline then
            -- search back to find the file name
            for curline = line-1, 0, -1 do
              local text = reseditor:GetLineDyn(curline):gsub("[\n\r]+$","")
              if not text:find("^%s") and wx.wxFileExists(text) then
                file = text
                break
              end
            end
          else
            file = text
            jumpline = 1
          end

          -- activate the file and the line number
          local editor = file and LoadFile(file,nil,true)
          if editor then
            editor:GotoLine(jumpline-1)
            editor:EnsureVisibleEnforcePolicy(jumpline-1)
            editor:SetFocus()
          end
          return
        end

        event:Skip()
      end)

    ide.config.editor.fold = fold
    self.reseditor = reseditor
  else
    if showaseditor then
      ide:GetDocument(reseditor):SetActive()
    else
      local index = nb:GetPageIndex(reseditor)
      if nb:GetSelection() ~= index then nb:SetSelection(index) end
    end
  end
  reseditor.replace = replace -- keep track of the current status
  reseditor:ShowLines(0, reseditor:GetLineCount()-1)
  reseditor:SetReadOnly(false)
  reseditor:SetTextDyn('')
  do -- update the preview name
    local nb, index = nb
    if showaseditor and ide:GetDocument(reseditor) then
      index, nb = ide:GetDocument(reseditor):GetTabIndex()
    else
      index, nb = nb:GetPageIndex(reseditor), nb
    end
    nb:SetPageText(index, previewText .. findText)
  end
  if not showaseditor and nb then -- show the bottom notebook if hidden
    local uimgr = ide:GetUIManager()
    if not uimgr:GetPane(nb):IsShown() then
      uimgr:GetPane(nb):Show(true)
      uimgr:Update()
    end
  end

  self:SetStatus(TR("Searching for '%s'."):format(findText)
    .." "..TR("Use %s to close."):format("`Escape`"))
  wx.wxSafeYield() -- allow the status to update

  local startdir, mask = self:GetScope()
  local completed = self:ProcInFiles(startdir, mask or "*", flags.SubDirs)

  -- reseditor may already be closed, so check if it's valid first
  if ide:IsValidCtrl(reseditor) then
    reseditor:GotoPos(reseditor:GetLength())
    reseditor:AppendTextDyn(("Searched for '%s'. "):format(findText))
    if not completed then reseditor:AppendTextDyn("Cancelled by the user. ") end
    reseditor:AppendTextDyn(("Found %d %s on %d %s in %d %s.")
      :format(
        self.occurrences, makePlural("instance", self.occurrences),
        self.lines, makePlural("line", self.lines),
        self.files, makePlural("file", self.files)))
    reseditor:EmptyUndoBuffer() -- don't undo the changes in the results
    reseditor:SetSavePoint() -- set unmodified status

    if completed and replace and self.occurrences > 0 then
      reseditor:AppendTextDyn("\n\n"
        .."Review the changes and save this preview to apply them.\n"
        .."You can also make other changes; only lines with : will be updated.\n"
        .."Context lines (if any) are used as safety checks during the update.")
      self:Replace(true, reseditor)
    else
      reseditor:SetReadOnly(true)
    end
    reseditor:EnsureVisibleEnforcePolicy(reseditor:GetLineCount()-1)
    reseditor.searchpreview = findText
  end

  -- the IDE may be closing during search, in which case simply return,
  -- as the controls are likely to be in some invalid state anyway
  if not ide:IsValidCtrl(self.oveditor) then return end

  local num = self.occurrences
  local msg = replace and TR("Replaced %d instance.", num) or TR("Found %d instance.", num)
  self:SetStatus(not completed and TR("Cancelled by the user.") or msg:format(num))
  self.oveditor:Destroy()
  self.oveditor = nil
  self.toolbar:UpdateWindowUI(wx.wxUPDATE_UI_FROMIDLE)

  if completed and ide.config.search.autohide then self:Hide() end
end

local icons = {
  find = {
    internal = {
      ID.FINDNEXT, ID.SEPARATOR,
      ID.FINDOPTDIRECTION, ID.FINDOPTWRAPWROUND, ID.FINDOPTSELECTION,
      ID.FINDOPTWORD, ID.FINDOPTCASE, ID.FINDOPTREGEX,
      ID.SEPARATOR, ID.FINDOPTSTATUS,
    },
    infiles = {
      ID.FIND, ID.SEPARATOR,
      ID.FINDOPTCONTEXT, ID.FINDOPTMULTIRESULTS, ID.FINDOPTWORD,
      ID.FINDOPTCASE, ID.FINDOPTREGEX,
      ID.SEPARATOR, ID.FINDOPTSUBDIR, ID.FINDOPTSYMLINK, ID.FINDOPTMAPPED,
      ID.FINDOPTSCOPE, ID.FINDSETDIR,
      ID.SEPARATOR, ID.FINDOPTSTATUS,
    },
  },
  replace = {
    internal = {
      ID.FINDNEXT, ID.FINDREPLACENEXT, ID.FINDREPLACEALL, ID.SEPARATOR,
      ID.FINDOPTDIRECTION, ID.FINDOPTWRAPWROUND, ID.FINDOPTSELECTION,
      ID.FINDOPTWORD, ID.FINDOPTCASE, ID.FINDOPTREGEX,
      ID.SEPARATOR, ID.FINDOPTSTATUS,
    },
    infiles = {
      ID.FIND, ID.FINDREPLACEALL, ID.SEPARATOR,
      ID.FINDOPTCONTEXT, ID.FINDOPTMULTIRESULTS, ID.FINDOPTWORD,
      ID.FINDOPTCASE, ID.FINDOPTREGEX,
      ID.SEPARATOR, ID.FINDOPTSUBDIR, ID.FINDOPTSYMLINK, ID.FINDOPTMAPPED,
      ID.FINDOPTSCOPE, ID.FINDSETDIR,
      ID.SEPARATOR, ID.FINDOPTSTATUS,
    },
  },
}

function findReplace:createToolbar()
  local ctrl, tb, scope, status =
    self.panel, self.toolbar, self.scope, self.status
  local icons = icons[self.replace and "replace" or "find"][self.infiles and "infiles" or "internal"]

  local iconsize = ide:GetBestIconSize()
  local toolBmpSize = wx.wxSize(iconsize, iconsize)
  tb:Freeze()
  tb:Clear()
  for _, id in ipairs(icons) do
    if id == ID.SEPARATOR then
      tb:AddSeparator()
    elseif id == ID.FINDOPTSCOPE then
      tb:AddControl(scope)
    elseif id == ID.FINDOPTSTATUS then
      tb:AddControl(status)
    else
      local iconmap = ide.config.toolbar.iconmap[id]
      if iconmap then
        local icon, description = unpack(iconmap)
        local isbitmap = type(icon) == "userdata" and icon:GetClassInfo():GetClassName() == "wxBitmap"
        local bitmap = isbitmap and icon or ide:GetBitmap(icon, "TOOLBAR", toolBmpSize)
        tb:AddTool(id, "", bitmap, (TR)(description))
      end
    end
  end

  local options = {
    [ID.FINDOPTDIRECTION] = 'Down',
    [ID.FINDOPTWRAPWROUND] = 'Wrap',
    [ID.FINDOPTWORD] = 'WholeWord',
    [ID.FINDOPTCASE] = 'MatchCase',
    [ID.FINDOPTREGEX] = 'RegularExpr',
    [ID.FINDOPTSUBDIR] = 'SubDirs',
    [ID.FINDOPTSYMLINK] = 'FollowSymlink',
    [ID.FINDOPTMAPPED] = 'MapDirs',
    [ID.FINDOPTCONTEXT] = 'Context',
    [ID.FINDOPTMULTIRESULTS] = 'MultiResults',
  }

  for id, var in pairs(options) do
    local tool = tb:FindTool(id)
    if tool then
      local flags = self:GetFlags()
      tool:SetSticky(flags[var])
      ctrl:Connect(id, wx.wxEVT_COMMAND_MENU_SELECTED,
        function ()
          flags[var] = not flags[var]
          self:SaveSettings()

          tb:FindTool(id):SetSticky(flags[var])
          tb:Refresh()
        end)
    end
  end

  local optseltool = tb:FindTool(ID.FINDOPTSELECTION)
  if optseltool then
    optseltool:SetSticky(self.inselection)
    local ed = self:GetEditor()
    local inselection = ed and ed:LineFromPosition(ed:GetSelectionStart()) ~= ed:LineFromPosition(ed:GetSelectionEnd())
    -- also check if the same editor previously had selection
    if not inselection and self.backfocus and self.backfocus.editor == ed then
      local bf = self.backfocus
      inselection = ed:LineFromPosition(bf.spos) ~= ed:LineFromPosition(bf.epos)
    end
    tb:EnableTool(ID.FINDOPTSELECTION, self.inselection or inselection)
    ctrl:Connect(ID.FINDOPTSELECTION, wx.wxEVT_COMMAND_MENU_SELECTED,
      function (event)
        self.inselection = not self.inselection
        tb:FindTool(event:GetId()):SetSticky(self.inselection)
        tb:Refresh()
      end)
  end

  tb:SetToolDropDown(ID.FINDSETDIR, true)
  tb:Connect(ID.FINDSETDIR, wxaui.wxEVT_COMMAND_AUITOOLBAR_TOOL_DROPDOWN, function(event)
      if event:IsDropDownClicked() then
        local menu = wx.wxMenu({})
        local pos = tb:GetToolRect(event:GetId()):GetBottomLeft()
        menu:Append(ID.FINDSETDIR, TR("Choose..."))
        menu:Append(ID.FINDSETTOPROJDIR, TR("Set To Project Directory"))
        menu:Enable(ID.FINDSETTOPROJDIR, ide:GetProject() ~= nil)
        menu:Connect(ID.FINDSETTOPROJDIR, wx.wxEVT_COMMAND_MENU_SELECTED,
          function()
            local _, mask = self:GetScope()
            self:refreshToolbar(self:SetScope(ide:GetProject(), mask))
          end)
        if #self.settings.slist > 0 then menu:AppendSeparator() end
        for i, text in ipairs(self.settings.slist) do
          local id = ID("findreplace.scope."..i)
          menu:Append(id, text)
          menu:Connect(id, wx.wxEVT_COMMAND_MENU_SELECTED,
            function() self:refreshToolbar(text) end)
        end
        menu:AppendSeparator()
        menu:Append(ID.RECENTSCOPECLEAR, TR("Clear Items"))
        menu:Enable(ID.RECENTSCOPECLEAR, #self.settings.slist > 0)
        menu:Connect(ID.RECENTSCOPECLEAR, wx.wxEVT_COMMAND_MENU_SELECTED,
          function()
            self.settings.slist = {}
            self:SaveSettings()
          end)
        tb:PopupMenu(menu, pos)
      else
        event:Skip()
      end
    end)

  tb:Realize()
  tb:Thaw()

  local sizer = ctrl:GetSizer()
  if sizer then sizer:Layout() end
end

function findReplace:refreshToolbar(value)
  local scope = self.scope
  value = value or self.scope:GetValue()
  self.scope:SetMinSize(wx.wxSize(scope:GetTextExtent(value..'AZ'), -1))
  self:createToolbar()
  self.scope:SetValue(value)
end

function findReplace:clearSelection()
  local editor = self.backfocus and ide:IsValidCtrl(self.backfocus.editor) and self.backfocus.editor
  if editor then
    -- always clear the search in selection indicator
    editor:SetIndicatorCurrent(indicator.SEARCHSELECTION)
    editor:IndicatorClearRange(0, editor:GetLength())
  end
end

function findReplace:createPanel()
  local ctrl = wx.wxPanel(ide:GetMainFrame(), wx.wxID_ANY, wx.wxDefaultPosition,
      wx.wxDefaultSize, wx.wxFULL_REPAINT_ON_RESIZE)
  local mgr = ide:GetUIManager()
  mgr:AddPane(ctrl, wxaui.wxAuiPaneInfo()
    :Name(searchpanel):CaptionVisible(false):PaneBorder(false):Hide())
  mgr:Update()

  local tb = wxaui.wxAuiToolBar(ctrl, wx.wxID_ANY,
    wx.wxDefaultPosition, wx.wxDefaultSize, wxaui.wxAUI_TB_PLAIN_BACKGROUND)
  local status = wx.wxStaticText(tb, wx.wxID_ANY, "")
  local scope = wx.wxTextCtrl(tb, wx.wxID_ANY, "",
    wx.wxDefaultPosition, wx.wxDefaultSize,
    wx.wxTE_PROCESS_ENTER + wx.wxTE_PROCESS_TAB + wx.wxBORDER_STATIC)
  -- limit the scope control height as it gets too large on Linux
  scope:SetMaxSize(wx.wxSize(-1, 22))

  self.panel = ctrl
  self.status = status
  self.toolbar = tb
  self.scope = scope

  self:createToolbar()

  local style, styledef = ide.config.styles, StylesGetDefault()
  local textcolor = wx.wxColour(unpack(style.text and style.text.fg or styledef.text.fg))
  local backcolor = wx.wxColour(unpack(style.text and style.text.bg or styledef.text.bg))
  local pancolor = tb:GetBackgroundColour()
  local borcolor = ide:GetUIManager():GetArtProvider():GetColour(wxaui.wxAUI_DOCKART_BORDER_COLOUR)
  local bpen = wx.wxPen(borcolor, 1, wx.wxSOLID)
  local bbrush = wx.wxBrush(pancolor, wx.wxSOLID)
  local tempctrl = ide:IsValidCtrl(ide:GetProjectTree()) and ide:GetProjectTree() or wx.wxTreeCtrl()
  local tfont = tempctrl:GetFont()
  
  -- don't increase font size on Linux as it gets too large
  tfont:SetPointSize(tfont:GetPointSize() + (ide.osname == 'Unix' and 0 or 1))

  local findCtrl = wx.wxTextCtrl(ctrl, wx.wxID_ANY, "",
    wx.wxDefaultPosition, wx.wxDefaultSize,
    wx.wxTE_PROCESS_ENTER + wx.wxTE_PROCESS_TAB + wx.wxBORDER_STATIC)
  local replaceCtrl = wx.wxTextCtrl(ctrl, wx.wxID_ANY, replaceHintText,
    wx.wxDefaultPosition, wx.wxDefaultSize,
    wx.wxTE_PROCESS_ENTER + wx.wxTE_PROCESS_TAB + wx.wxBORDER_STATIC)
  self.ac = {[findCtrl:GetId()] = {}, [replaceCtrl:GetId()] = {}, [scope:GetId()] = {}}

  local findSizer = wx.wxBoxSizer(wx.wxHORIZONTAL)
  findSizer:Add(findCtrl, 1, wx.wxLEFT + wx.wxRIGHT + wx.wxALIGN_LEFT + wx.wxEXPAND + wx.wxFIXED_MINSIZE, 1)
  findSizer:Add(replaceCtrl, 1, wx.wxLEFT + wx.wxRIGHT + wx.wxALIGN_LEFT + wx.wxEXPAND + wx.wxFIXED_MINSIZE, 1)
  findSizer:Hide(1)

  local mainSizer = wx.wxBoxSizer(wx.wxVERTICAL)
  mainSizer:Add(tb, 0, wx.wxTOP + wx.wxLEFT + wx.wxRIGHT + wx.wxALIGN_LEFT + wx.wxEXPAND, 2)
  mainSizer:Add(findSizer, 0, wx.wxALL + wx.wxALIGN_LEFT + wx.wxEXPAND, 2)

  ctrl:SetSizer(mainSizer)
  ctrl:GetSizer():Fit(ctrl)

  -- for _, control in ipairs({findCtrl, replaceCtrl}) do
    -- control:SetBackgroundColour(backcolor)
    -- control:SetForegroundColour(textcolor)
    -- control:SetFont(tfont)
  -- end
  -- scope:SetBackgroundColour(pancolor) -- set toolbar background
  -- scope:SetFont(tfont)
  -- status:SetFont(tfont)

  local function updateLists()
    PrependStringToArray(self.settings.flist, findCtrl:GetValue())
    if self.replace then
      local replaceText = replaceCtrl:GetValue()
      if replaceText == replaceHintText then replaceText = "" end
      PrependStringToArray(self.settings.rlist, replaceText)
    end
    if self.infiles then
      PrependStringToArray(self.settings.slist, self.scope:GetValue())
    end
    self:SaveSettings()
    return true
  end

  local function findNext()
    updateLists()
    if findReplace.infiles then
      findReplace:RunInFiles(false)
    else
      local reverse = (wx.wxGetKeyState(wx.WXK_SHIFT)
          and not wx.wxGetKeyState(wx.WXK_ALT) and not wx.wxGetKeyState(wx.WXK_CONTROL))
      findReplace:Find(reverse)
    end
  end

  local function autoComplete(event)
    if not ide.config.search.autocomplete then return end

    local obj = event:GetEventObject():DynamicCast('wxTextCtrl')
    local ac = self.ac[obj:GetId()]
    if not ac then return end

    local keycode, needac = ac.lastkeycode, ac.needautocomplete
    if needac then ac.needautocomplete = false end
    if not needac or not keycode then return end

    -- if the last key was Delete or Backspace, don't autocomplete
    if keycode == wx.WXK_DELETE or keycode == wx.WXK_BACK then return end

    -- find match for the current text and add it to the control
    local value = obj:GetValue()
    if not value or #value == 0 then return end

    local patt, match = "^"..q(value)
    for _, v in ipairs(
      obj:GetId() == self.findCtrl:GetId() and self.settings.flist or
      obj:GetId() == self.replaceCtrl:GetId() and self.settings.rlist or
      {}
    ) do
      if v:find(patt) then match = v; break end
    end
    if match then
      obj:ChangeValue(match)
      obj:SetSelection(#value, #match)
    end
  end

  local function findIncremental(event)
    if not self.infiles and self.backfocus and self.backfocus.position then
      local editor = self:GetEditor()
      if editor then
        editor:SetSelection(self.backfocus.position, self.backfocus.position)
      end
    end
    -- don't search when used with "infiles", but still trigger autocomplete
    if self.infiles or self:Find() then
      self.ac[event:GetEventObject():DynamicCast('wxTextCtrl'):GetId()].needautocomplete = true
    end
  end

  local function findReplaceNext()
    updateLists()
    if findReplace.replace then
      if findReplace.infiles then
        findReplace:RunInFiles(true)
      else
        local replaceAll = (wx.wxGetKeyState(wx.WXK_ALT)
          and not wx.wxGetKeyState(wx.WXK_SHIFT) and not wx.wxGetKeyState(wx.WXK_CONTROL))
        findReplace:Replace(replaceAll)
      end
    end
  end

  local function findReplaceAll()
    updateLists()
    if findReplace.replace then
      if findReplace.infiles then
        findReplace:RunInFiles(true)
      else
        findReplace:Replace(true)
      end
    end
  end

  local function onPanelPaint()
    local dc = wx.wxBufferedPaintDC(ctrl)
    local psize = ctrl:GetClientSize()
    dc:SetBrush(bbrush)
    dc:SetPen(bpen)
    dc:DrawRectangle(0, 0, psize:GetWidth(), psize:GetHeight())
    dc:SetPen(wx.wxNullPen)
    dc:SetBrush(wx.wxNullBrush)
    dc:delete()
  end

  ctrl:Connect(wx.wxEVT_PAINT, onPanelPaint)
  ctrl:Connect(wx.wxEVT_ERASE_BACKGROUND, function() end)

  local taborder = {findCtrl, replaceCtrl, scope}
  local function keyHandle(event)
    local keycode = event:GetKeyCode()
    self.ac[event:GetEventObject():DynamicCast('wxTextCtrl'):GetId()].lastkeycode = keycode
    if keycode == wx.WXK_ESCAPE then
      self:Hide(event:ShiftDown())
    elseif keycode == wx.WXK_TAB then
      local id = event:GetId()
      local order, pos = {}
      for _, v in ipairs(taborder) do
        if v:IsEnabled() and v:IsShown() then table.insert(order, v) end
        if v:GetId() == id then pos = #order end
      end
      if not pos then return end
      pos = pos + (event:ShiftDown() and -1 or 1)
      if pos == 0 then pos = #order
      elseif pos > #order then pos = 1
      end
      order[pos]:SetFocus()
      if order[pos] ~= scope then order[pos]:SetSelection(-1, -1) end
    else
      event:Skip()
    end
  end

  -- remember the current position in the editor when setting focus on find
  local function refreshEditorInfo()
    local ed = self:GetEditor()
    if ed and ed ~= self.oveditor then
      local spos, epos = ed:GetSelectionStart(), ed:GetSelectionEnd()
      if not self.backfocus or self.backfocus.editor ~= ed then
        self:clearSelection() -- clear current selection if switching editors
        self.backfocus = { editor = ed, spos = spos, epos = epos }
      end
      local bf = self.backfocus
      bf.position = spos == epos and ed:GetCurrentPos() or spos
    end
  end
  findCtrl:Connect(wx.wxEVT_SET_FOCUS,
    function(event)
      event:Skip()
      refreshEditorInfo()
    end)
  findCtrl:Connect(wx.wxEVT_COMMAND_TEXT_ENTER, findNext)
  findCtrl:Connect(wx.wxEVT_COMMAND_TEXT_UPDATED, findIncremental)
  findCtrl:Connect(wx.wxEVT_KEY_DOWN, keyHandle)
  replaceCtrl:Connect(wx.wxEVT_SET_FOCUS, function(event)
      event:Skip()
      refreshEditorInfo()
      -- hide the replace hint; should be done with SetHint method,
      -- but it's not yet available in wxlua 2.8.12
      if replaceCtrl:GetValue() == replaceHintText then replaceCtrl:ChangeValue('') end
    end)
  replaceCtrl:Connect(wx.wxEVT_COMMAND_TEXT_ENTER, findReplaceNext)
  replaceCtrl:Connect(wx.wxEVT_COMMAND_TEXT_UPDATED, function(event)
      self.ac[event:GetEventObject():DynamicCast('wxTextCtrl'):GetId()].needautocomplete = true
    end)
  replaceCtrl:Connect(wx.wxEVT_KEY_DOWN, keyHandle)

  -- autocomplete for find/replace can be done from TEXT_UPDATED event,
  -- but SetSelection doesn't work from TEXT_UPDATED event on Linux,
  -- which makes it impossible to select the suggested part.
  -- IDLE event is used instead to provide autocomplete suggestions.
  findCtrl:Connect(wx.wxEVT_IDLE, autoComplete)
  replaceCtrl:Connect(wx.wxEVT_IDLE, autoComplete)

  scope:Connect(wx.wxEVT_COMMAND_TEXT_ENTER, findNext)
  scope:Connect(wx.wxEVT_KEY_DOWN, keyHandle)

  local function notSearching(event) event:Enable(not self.oveditor) end
  ctrl:Connect(ID.FIND, wx.wxEVT_UPDATE_UI, notSearching)
  ctrl:Connect(ID.FINDNEXT, wx.wxEVT_UPDATE_UI, notSearching)
  ctrl:Connect(ID.FINDREPLACENEXT, wx.wxEVT_UPDATE_UI, notSearching)
  ctrl:Connect(ID.FINDREPLACEALL, wx.wxEVT_UPDATE_UI, notSearching)

  ctrl:Connect(ID.FIND, wx.wxEVT_COMMAND_MENU_SELECTED, findNext)
  ctrl:Connect(ID.FINDNEXT, wx.wxEVT_COMMAND_MENU_SELECTED, findNext)
  ctrl:Connect(ID.FINDREPLACENEXT, wx.wxEVT_COMMAND_MENU_SELECTED, findReplaceNext)
  ctrl:Connect(ID.FINDREPLACEALL, wx.wxEVT_COMMAND_MENU_SELECTED, findReplaceAll)

  ctrl:Connect(ID.FINDSETDIR, wx.wxEVT_COMMAND_MENU_SELECTED,
    function()
      local dir, mask = self:GetScope()
      local filePicker = wx.wxDirDialog(ctrl, TR("Choose a search directory"),
        dir or wx.wxGetCwd(), wx.wxFLP_USE_TEXTCTRL)
      if filePicker:ShowModal(true) == wx.wxID_OK then
        self:refreshToolbar(self:SetScope(FixDir(filePicker:GetPath()), mask))
      end
    end)

  self.findCtrl = findCtrl
  self.replaceCtrl = replaceCtrl
  self.findSizer = findSizer
end

function findReplace:refreshPanel(replace, infiles)
  if not self.panel then self:createPanel() end

  self:GetEditor(true) -- remember the current editor

  local ctrl = self.panel

  -- check if a proper pane is already populated
  if self.replace ~= replace or self.infiles ~= infiles then
    self.replace = replace
    self.infiles = infiles

    if replace then
      self.findSizer:Show(1)
      if self.replaceCtrl:GetValue() == '' then
        self.replaceCtrl:ChangeValue(replaceHintText)
      end
    else
      self.findSizer:Hide(1)
    end
    self.findSizer:Layout()

    self.scope:Show(infiles)
  end

  local value = self.scope:GetValue()
  local ed = self:GetEditor()
  if not value or #value == 0 then
    local doc = ed and ide:GetDocument(ed)
    local ext = doc and doc:GetFileExt() or ""
    local proj = ide:GetProject()
    value = (proj and self:GetScopeMRU(proj..sep) or
      self:SetScope(proj or wx.wxGetCwd(), '*.'..(#ext > 0 and ext or '*')))
  end
  if ed then -- check if there is any selection
    local s, e = ed:GetSelectionStart(), ed:GetSelectionEnd()
    self.inselection = (ide.config.search.autoinselection
      and ed:LineFromPosition(s) ~= ed:LineFromPosition(e))
    if self.inselection then
      ed:SetIndicatorCurrent(indicator.SEARCHSELECTION)
      ed:IndicatorClearRange(0, ed:GetLength())
      ed:IndicatorFillRange(s, e-s)
    end
  end
  self:refreshToolbar(value)

  local mgr = ide:GetUIManager()
  local pane = mgr:GetPane(searchpanel)
  if not pane:IsShown() then
    local size = ctrl:GetSize()
    pane:Dock():Bottom():BestSize(size):MinSize(size):Layer(0):Row(1):Show()
    mgr:Update()

    self:SetStatus(TR("Use %s to close."):format("`Escape`"))
  end

  -- set value from the current selection (if any)
  self.findCtrl:ChangeValue(self:GetSelection() or self.findCtrl:GetValue())

  -- reset search when re-creating dialog to avoid modifying selected
  -- fragment after successful search and updated replacement
  self.foundString = false
  self.findCtrl:SetFocus()
  self.findCtrl:SetSelection(-1, -1) -- select the content
end

function findReplace:RefreshResults(editor)
  if not ide:IsValidCtrl(editor) or not editor.searchpreview then return end

  self:Show(false, true) -- always show "Find" when refreshing
  self:SetFind(editor.searchpreview)
  editor:SetFocus() -- set the focus on the editor as it may be in an inactive tab
  self.reseditor = editor:DynamicCast("wxStyledTextCtrl") -- show the results in the same tab
  self:RunInFiles(false) -- only refresh search results, no replace
end

function findReplace:Show(replace,infiles)
  self:refreshPanel(replace,infiles)
end

function findReplace:IsShown()
  local pane = ide:GetUIManager():GetPane(searchpanel)
  return pane:IsOk() and pane:IsShown()
end

function findReplace:Hide(restorepos)
  local ctrl = self.panel:FindFocus()
  if not ctrl or ctrl:GetParent():GetId() ~= self.panel:GetId() then
    -- if focus outside of the search panel, do nothing
  elseif self.backfocus and ide:IsValidCtrl(self.backfocus.editor) then
    local editor = self.backfocus.editor
    -- restore original position for Shift-Esc or failed search
    if restorepos or self.foundString == false then
      editor:SetSelection(self.backfocus.spos, self.backfocus.epos)
    end
    editor:SetFocus()
  elseif self:IsPreview(self.reseditor) then -- there is a preview, go there
    self.reseditor:SetFocus()
  end

  self:clearSelection()

  local mgr = ide:GetUIManager()
  mgr:GetPane(searchpanel):Hide()
  mgr:Update()
end

local package = ide:AddPackage('core.findreplace', {
    onProjectLoad = function()
      if not findReplace.panel then return end -- not set yet
      local _, mask = findReplace:GetScope()
      local proj = ide:GetProject()
      -- find the last used scope for the same project on the scope history
      findReplace:refreshToolbar(findReplace:GetScopeMRU(proj..sep)
        or findReplace:SetScope(proj, mask))
    end,

    onEditorPreSave = function(self, editor, filePath)
      if not findReplace:IsPreview(editor) then return end

      local isModified = editor:GetModify()
      if editor.replace and isModified then
        findReplace:SetStatus("")

        local line = wx.wxNOT_FOUND
        local oveditor = ide:CreateStyledTextCtrl(findReplace.panel, wx.wxID_ANY,
          wx.wxDefaultPosition, wx.wxSize(0,0), wx.wxBORDER_NONE)
        local files, lines = 0, 0
        local report
        while true do
          -- for each marker that marks a file (MarkerNext)
          line = editor:MarkerNext(line + 1, FILE_MARKER_VALUE)
          if line == wx.wxNOT_FOUND then break end

          local fname = getRawLine(editor, line) -- get the file name
          local filetext, err = FileRead(fname)
          local mismatch = false
          if filetext then
            findReplace:SetStatus(GetFileName(fname))
            wx.wxSafeYield()

            oveditor:SetTextDyn(filetext)
            while true do -- for each line following the file name
              line = line + 1
              local text = getRawLine(editor, line)
              local lnum, lmark, ltext = text:match("^%s*(%d+)([ :]) (.*)")
              if lnum then
                lnum = tonumber(lnum)
                if lmark == ':' then -- if the change line, then apply the change
                  local pos = oveditor:PositionFromLine(lnum-1)
                  if pos == wx.wxNOT_FOUND then
                    mismatch = lnum
                    break
                  end
                  oveditor:SetTargetStart(pos)
                  oveditor:SetTargetEnd(pos+#getRawLine(oveditor, lnum-1))
                  oveditor:ReplaceTarget(ltext)
                  lines = lines + 1
                -- if the context line, then check the context
                elseif getRawLine(oveditor, lnum-1) ~= ltext then
                  mismatch = lnum
                  break
                end
              -- if not placeholder line " ...", then abort
              elseif not text:find("^%s*%.+$") then
                break
              end
            end
            if lines > 0 and not mismatch then -- save the file
              local ok
              ok, err = FileWrite(fname, oveditor:GetTextDyn())
              if ok then files = files + 1 end
            end
          end
          if err or mismatch then
            report = (report or "") .. (("\n%s: %s")
              :format(fname, mismatch and "mismatch on line "..mismatch or err))
          end
        end
        oveditor:Destroy() -- destroy the editor to release its memory
        if report then editor:AppendTextDyn("\n"..report) end
        editor:AppendTextDyn(("\n\nUpdated %d %s in %d %s.")
          :format(
            lines, makePlural("line", lines),
            files, makePlural("file", files)))
        editor:EnsureVisibleEnforcePolicy(editor:GetLineCount()-1)
        editor:SetSavePoint() -- set unmodified status when done
        findReplace:SetStatus(TR("Updated %d file.", files):format(files))
        return false

      -- don't offer to save file if called from SaveFile;
      -- can still be used with explicit SaveFileAs
      elseif not filePath and not isModified then
        return false
      end
    end
  })

function findReplace:SaveSettings() package:SetSettings(self.settings) end
MergeSettings(findReplace.settings, package:GetSettings())
