1276 lines
46 KiB
Lua
1276 lines
46 KiB
Lua
-- Copyright 2011-15 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
|
|
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 NOTFOUND = -1
|
|
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 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, iff(fAll, selStart, selEnd)))
|
|
else -- reverse the range for the backward search
|
|
e = flags.StartPos or 0
|
|
s = math.max(e, math.min(flags.EndPos or len, iff(fAll, selEnd, 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
|
|
-- trip 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 = iff(reverse, not self:GetFlags().Down, 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 == NOTFOUND) and self:GetFlags().Wrap then
|
|
editor:SetTargetStart(iff(fDown, bf.spos or 0, bf.epos or editor:GetLength()))
|
|
editor:SetTargetEnd(iff(fDown, bf.epos or editor:GetLength(), 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 == NOTFOUND then
|
|
self.foundString = false
|
|
msg = TR("Text not found.")
|
|
else
|
|
self.foundString = true
|
|
local start = editor:GetTargetStart()
|
|
local finish = editor:GetTargetEnd()
|
|
editor:ShowPosEnforcePolicy(finish)
|
|
editor:SetSelection(start, finish)
|
|
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 == NOTFOUND 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"),
|
|
}
|
|
|
|
-- 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 ~= NOTFOUND then
|
|
editor:BeginUndoAction()
|
|
while posFind ~= NOTFOUND 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) ~= NOTFOUND 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(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] = 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 files = coroutine.wrap(function() FileSysGetRecursive(startdir, subdirs, mask, {yield = true, folder = false}) end)
|
|
while true do
|
|
local file = files()
|
|
if not file then break end
|
|
|
|
if checkBinary(GetFileExt(file)) ~= true then
|
|
self.curfilename = file
|
|
local filetext, err = FileRead(file, firstReadSize)
|
|
if not filetext then
|
|
DisplayOutputLn(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
|
|
|
|
-- give time to the UI to refresh
|
|
ide:Yield()
|
|
-- 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
|
|
if not (ok and mgr:GetPane(searchpanel):IsShown())
|
|
-- and check that the search results tab is still open
|
|
or not ide:IsValidCtrl(self.reseditor) 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 ctrl = ide:GetMainFrame():FindFocus()
|
|
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
|
|
if wx.wxGetKeyState(wx.WXK_SHIFT) and wx.wxGetKeyState(wx.WXK_CONTROL) then
|
|
editor:FoldSome()
|
|
elseif header then
|
|
editor:ToggleFold(line)
|
|
end
|
|
end)
|
|
|
|
-- mark as searchpreview to allow AddPage to add "close" button
|
|
reseditor.searchpreview = findText
|
|
nb:AddPage(reseditor, previewText, true)
|
|
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()
|
|
local margin = 0
|
|
for m = 0, ide.MAXMARGIN do margin = margin + reseditor:GetMarginWidth(m) end
|
|
if point:GetX() <= margin 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 = showaseditor and ide:GetEditorNotebook() or nb
|
|
nb:SetPageText(nb:GetPageIndex(reseditor), 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))
|
|
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
|
|
|
|
self:SetStatus(not completed and TR("Cancelled by the user.")
|
|
or TR("Found %d instance.", self.occurrences):format(self.occurrences))
|
|
self.oveditor:Destroy()
|
|
self.oveditor = nil
|
|
self.toolbar:UpdateWindowUI(wx.wxUPDATE_UI_FROMIDLE)
|
|
|
|
-- return focus to the control that had it if it's on the search panel
|
|
-- (as it could be changed by added results tab)
|
|
if ctrl and (ctrl:GetParent():GetId() == self.panel:GetId() or not showaseditor) then
|
|
-- set the focus temporarily on the search results tab as this provides a workaround
|
|
-- for the cursor disappearing in Search/Replace controls after results shown
|
|
-- in the same tab (somehow caused by `oveditor:Destroy()` call).
|
|
if ide:IsValidCtrl(reseditor) then reseditor:SetFocus() end
|
|
ctrl:SetFocus()
|
|
end
|
|
|
|
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_FINDNEXT, ID_SEPARATOR,
|
|
ID_FINDOPTCONTEXT, ID_FINDOPTMULTIRESULTS, ID_FINDOPTWORD,
|
|
ID_FINDOPTCASE, ID_FINDOPTREGEX, ID_FINDOPTSUBDIR,
|
|
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_FINDNEXT, ID_FINDREPLACEALL, ID_SEPARATOR,
|
|
ID_FINDOPTCONTEXT, ID_FINDOPTMULTIRESULTS, ID_FINDOPTWORD,
|
|
ID_FINDOPTCASE, ID_FINDOPTREGEX, ID_FINDOPTSUBDIR,
|
|
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 toolBmpSize = wx.wxSize(16, 16)
|
|
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_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)
|
|
tb:EnableTool(ID_FINDOPTSELECTION, self.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: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.fg or styledef.text.fg))
|
|
local backcolor = wx.wxColour(unpack(style.text.bg or styledef.text.bg))
|
|
local pancolor = tb:GetBackgroundColour()
|
|
local borcolor = ide:GetUIManager():GetArtProvider():GetColor(wxaui.wxAUI_DOCKART_BORDER_COLOUR)
|
|
local bpen = wx.wxPen(borcolor, 1, wx.wxSOLID)
|
|
local bbrush = wx.wxBrush(pancolor, wx.wxSOLID)
|
|
local tfont = ide:GetProjectTree():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
|
|
findReplace:Find()
|
|
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)
|
|
-- don't do any incremental search when search in selection
|
|
if self.inselection then return end
|
|
|
|
if not self.infiles and self.backfocus and self.backfocus.position then
|
|
self:GetEditor():SetSelection(self.backfocus.position, self.backfocus.position)
|
|
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.backfocus = { editor = ed, spos = spos, epos = epos }
|
|
end
|
|
local bf = self.backfocus
|
|
bf.position = spos == epos and ed:GetCurrentPos() or spos
|
|
local inselection = ed:LineFromPosition(spos) ~= ed:LineFromPosition(epos)
|
|
|
|
-- when the focus is changed, don't remove current "inselection" status as the
|
|
-- selection may change to highlight the match; not doing this makes it difficult
|
|
-- to switch between searching and replacing without losing the current match
|
|
if inselection and (not self.inselection or bf.spos ~= spos or bf.epos ~= epos) then
|
|
bf.spos = spos
|
|
bf.epos = epos
|
|
self.inselection = inselection
|
|
self:refreshToolbar()
|
|
end
|
|
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_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_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 = ide:GetEditor()
|
|
if ed and (not value or #value == 0) then
|
|
local doc = ide:GetDocument(ed)
|
|
local ext = doc:GetFileExt()
|
|
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
|
|
self.backfocus = nil
|
|
self.inselection = ed:LineFromPosition(ed:GetSelectionStart()) ~=
|
|
ed:LineFromPosition(ed:GetSelectionEnd())
|
|
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: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
|
|
|
|
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 = NOTFOUND
|
|
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 == NOTFOUND 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 == NOTFOUND 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())
|