Files
OpenRA/src/editor/findreplace.lua
Paul Kulchenko cb56311090 Fixed Find/Replace checkboxes on OSX (closes #63).
Fixed formatting/alignment of fields in Find/Replace dialog on OSX.
2012-10-16 15:08:14 -07:00

508 lines
18 KiB
Lua

-- authors: Lomtik Software (J. Winwood & John Labenski)
-- Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
local ide = ide
ide.findReplace = {
dialog = nil, -- the wxDialog for find/replace
replace = false, -- is it a find or replace dialog
infiles = false,
fWholeWord = false, -- match whole words
fMatchCase = false, -- case sensitive
fRegularExpr = false, -- use regex
fWrap = true, -- search wraps around
fDown = true, -- search downwards in doc
fSubDirs = false, -- search in subdirectories
fMakeBak = true, -- make bak files for replace in files
findTextArray = {}, -- array of last entered find text
findText = "", -- string to find
replaceTextArray = {}, -- array of last entered replace text
replaceText = "", -- string to replace find string with
filemaskText = "*.*",
filemaskTextArray= {},
filedirText = "",
filedirTextArray = {},
foundString = false, -- was the string found for the last search
oveditor = nil,
curfilename = "", -- for search in files
occurrences = 0,
-- HasText() is there a string to search for
-- GetSelectedString() get currently selected string if it's on one line
-- FindString(reverse) find the findText string
-- Show(replace) create the dialog
-- GetEditor() which editor to use
}
local findReplace = ide.findReplace
function findReplace:GetEditor()
return findReplace.oveditor or GetEditor()
end
-------------------- Find replace dialog
local function setSearchFlags(editor)
local flags = 0
if findReplace.fWholeWord then flags = wxstc.wxSTC_FIND_WHOLEWORD end
if findReplace.fMatchCase then flags = flags + wxstc.wxSTC_FIND_MATCHCASE end
if findReplace.fRegularExpr then flags = flags + wxstc.wxSTC_FIND_REGEXP end
editor:SetSearchFlags(flags)
end
local function setTarget(editor, fDown, fInclude)
local selStart = editor:GetSelectionStart()
local selEnd = editor:GetSelectionEnd()
local len = editor:GetLength()
local s, e
if fDown then
e= len
s = iff(fInclude, selStart, selEnd +1)
else
s = 0
e = iff(fInclude, selEnd, selStart-1)
end
if not fDown and not fInclude then s, e = e, s end
editor:SetTargetStart(s)
editor:SetTargetEnd(e)
return e
end
local function setTargetAll(editor)
local s = 0
local e = editor:GetLength()
editor:SetTargetStart(s)
editor:SetTargetEnd(e)
return e
end
function findReplace:HasText()
return (findReplace.findText ~= nil) and (string.len(findReplace.findText) > 0)
end
function findReplace:GetSelectedString()
local editor = findReplace:GetEditor()
if editor then
local startSel = editor:GetSelectionStart()
local endSel = editor:GetSelectionEnd()
if (startSel ~= endSel) and (editor:LineFromPosition(startSel) == editor:LineFromPosition(endSel)) then
findReplace.findText = editor:GetSelectedText()
findReplace.foundString = true
end
end
return editor and findReplace.foundString
end
function findReplace:FindString(reverse)
if findReplace:HasText() then
local editor = findReplace:GetEditor()
local fDown = iff(reverse, not findReplace.fDown, findReplace.fDown)
local lenFind = string.len(findReplace.findText)
setSearchFlags(editor)
setTarget(editor, fDown)
local posFind = editor:SearchInTarget(findReplace.findText)
if (posFind == -1) and findReplace.fWrap then
editor:SetTargetStart(iff(fDown, 0, editor:GetLength()))
editor:SetTargetEnd(iff(fDown, editor:GetLength(), 0))
posFind = editor:SearchInTarget(findReplace.findText)
end
if posFind == -1 then
findReplace.foundString = false
ide.frame:SetStatusText("Text not found.")
else
findReplace.foundString = true
local start = editor:GetTargetStart()
local finish = editor:GetTargetEnd()
EnsureRangeVisible(start, finish)
editor:SetSelection(start, finish)
end
end
end
-- returns if something was found
-- [inFileRegister(pos)] passing function will
-- register every position item was found
-- supposed for "Search/Replace in Files"
function findReplace:FindStringAll(inFileRegister)
local found = false
if findReplace:HasText() then
local findLen = string.len(findReplace.findText)
local editor = findReplace:GetEditor()
local e = setTargetAll(editor)
setSearchFlags(editor)
local posFind = editor:SearchInTarget(findReplace.findText)
if (posFind ~= -1) then
while posFind ~= -1 do
inFileRegister(posFind)
editor:SetTargetStart(posFind + findLen)
editor:SetTargetEnd(e)
posFind = editor:SearchInTarget(findReplace.findText)
end
found = true
end
end
return found
end
-- returns if replacements were done
-- [inFileRegister(pos)] passing function will disable "undo"
-- registers every position item was found
-- supposed for "Search/Replace in Files"
function findReplace:ReplaceString(fReplaceAll,inFileRegister)
local replaced = false
if findReplace:HasText() then
local replaceLen = string.len(findReplace.replaceText)
local findLen = string.len(findReplace.findText)
local editor = findReplace:GetEditor()
local endTarget = inFileRegister and setTargetAll(editor) or
setTarget(editor, findReplace.fDown, fReplaceAll)
if fReplaceAll then
setSearchFlags(editor)
local posFind = editor:SearchInTarget(findReplace.findText)
if (posFind ~= -1) then
if(not inFileRegister) then editor:BeginUndoAction() end
while posFind ~= -1 do
if(inFileRegister) then inFileRegister(posFind) end
editor:ReplaceTarget(findReplace.replaceText)
editor:SetTargetStart(posFind + replaceLen)
endTarget = endTarget + replaceLen - findLen
editor:SetTargetEnd(endTarget)
posFind = editor:SearchInTarget(findReplace.findText)
end
if(not inFileRegister) then editor:EndUndoAction() end
replaced = true
end
else
if findReplace.foundString then
local start = editor:GetSelectionStart()
editor:ReplaceSelection(findReplace.replaceText)
editor:SetSelection(start, start + replaceLen)
findReplace.foundString = false
replaced = true
end
findReplace:FindString()
end
end
return replaced
end
local function onFileRegister(pos)
local editor = findReplace.oveditor
local line = editor:LineFromPosition(pos)
local linepos = pos - editor:PositionFromLine(line)
local result = "("..(line+1)..","..(linepos+1).."): "..editor:GetLine(line)
DisplayOutput(findReplace.curfilename..result)
findReplace.occurrences = findReplace.occurrences + 1
end
local function ProcInFiles(startdir,mask,subdirs,replace)
if (subdirs) then
local dirs = FileSysGet(startdir..string_Pathsep.."*",wx.wxDIR)
for _,dir in ipairs(dirs) do
ProcInFiles(dir,mask,true,replace)
end
end
local files = FileSysGet(startdir..string_Pathsep..mask,wx.wxFILE)
for _,file in ipairs(files) do
-- ignore .bak files when replacing and asked to store .bak files
if not (replace and findReplace.fMakeBak and file:find('.bak$')) then
findReplace.curfilename = file
local filetext = FileRead(file)
if filetext then
findReplace.oveditor:SetText(filetext)
if replace then
-- check if anything replaced, store changed content, make .bak
if findReplace:ReplaceString(true,onFileRegister)
and findReplace.fMakeBak and FileWrite(file..".bak",filetext) then
FileWrite(file,findReplace.oveditor:GetText())
end
else
findReplace:FindStringAll(onFileRegister)
end
end
end
end
end
function findReplace:RunInFiles(replace)
if (not findReplace:HasText()) then
return
end
findReplace.oveditor = wxstc.wxStyledTextCtrl(findReplace.dialog, wx.wxID_ANY,
wx.wxDefaultPosition, wx.wxSize(1,1), wx.wxBORDER_STATIC)
findReplace.occurrences = 0
local fname = wx.wxFileName(findReplace.filedirText)
local startdir = findReplace.filedirText
DisplayOutput("FindInFiles: "..(replace and "Replacing" or "Searching for").." '"..findReplace.findText.."'.\n")
ProcInFiles(startdir, findReplace.filemaskText, findReplace.fSubDirs, replace)
DisplayOutput("FindInFiles: "..findReplace.occurrences.." instance(s) have been "..
(replace and "replaced" or "found")..".\n")
findReplace.oveditor = nil
end
local function createFindReplaceDialog(replace,infiles)
local ID_FIND_NEXT = 1
local ID_REPLACE = 2
local ID_REPLACE_ALL = 3
local ID_SETDIR = 4
findReplace.replace = replace
findReplace.infiles = infiles
local findDialog = wx.wxDialog(ide.frame, wx.wxID_ANY, infiles and "Find In Files" or "Find",
wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxDEFAULT_DIALOG_STYLE)
-- Create right hand buttons and sizer
local findButton = wx.wxButton(findDialog, ID_FIND_NEXT, infiles and "&Find All" or "&Find Next")
findButton:SetDefault()
local replaceButton = wx.wxButton(findDialog, ID_REPLACE, infiles and replace and "&Replace All" or "&Replace")
local replaceAllButton = nil
if (replace and not infiles) then
replaceAllButton = wx.wxButton(findDialog, ID_REPLACE_ALL, "Replace &All")
end
local cancelButton = wx.wxButton(findDialog, wx.wxID_CANCEL, "Cancel")
local buttonsSizer = wx.wxBoxSizer(wx.wxVERTICAL)
buttonsSizer:Add(findButton, 0, wx.wxALL + wx.wxGROW + wx.wxCENTER, 3)
buttonsSizer:Add(replaceButton, 0, wx.wxALL + wx.wxGROW + wx.wxCENTER, 3)
if replaceAllButton then
buttonsSizer:Add(replaceAllButton, 0, wx.wxALL + wx.wxGROW + wx.wxCENTER, 3)
end
buttonsSizer:Add(cancelButton, 0, wx.wxALL + wx.wxGROW + wx.wxCENTER, 3)
-- Create find/replace text entry sizer
local findStatText = wx.wxStaticText( findDialog, wx.wxID_ANY, "Find: ")
local findTextCombo = wx.wxComboBox(findDialog, wx.wxID_ANY, findReplace.findText,
wx.wxDefaultPosition, wx.wxDefaultSize, findReplace.findTextArray, wx.wxCB_DROPDOWN)
findTextCombo:SetFocus()
local infilesMaskStat,infilesMaskCombo
local infilesDirStat,infilesDirCombo,infilesDirButton
if (infiles) then
infilesMaskStat = wx.wxStaticText( findDialog, wx.wxID_ANY, "File Type: ")
infilesMaskCombo = wx.wxComboBox(findDialog, wx.wxID_ANY, findReplace.filemaskText,
wx.wxDefaultPosition, wx.wxDefaultSize, findReplace.filemaskTextArray)
local fname = GetEditorFileAndCurInfo(true)
if (fname) then
findReplace.filedirText = fname:GetPath(wx.wxPATH_GET_VOLUME)
end
infilesDirStat = wx.wxStaticText( findDialog, wx.wxID_ANY, "Directory: ")
infilesDirCombo = wx.wxComboBox(findDialog, wx.wxID_ANY, findReplace.filedirText, wx.wxDefaultPosition, wx.wxDefaultSize, findReplace.filedirTextArray)
infilesDirButton = wx.wxButton(findDialog, ID_SETDIR, "...",wx.wxDefaultPosition, wx.wxSize(26,20))
end
local replaceStatText, replaceTextCombo
if (replace) then
replaceStatText = wx.wxStaticText( findDialog, wx.wxID_ANY, "Replace: ")
replaceTextCombo = wx.wxComboBox(findDialog, wx.wxID_ANY, findReplace.replaceText, wx.wxDefaultPosition, wx.wxDefaultSize, findReplace.replaceTextArray)
end
local findReplaceSizer = wx.wxFlexGridSizer(2, 3, 0, 0)
findReplaceSizer:AddGrowableCol(1)
findReplaceSizer:Add(findStatText, 0, wx.wxALL + wx.wxALIGN_RIGHT + wx.wxALIGN_CENTER_VERTICAL, 0)
findReplaceSizer:Add(findTextCombo, 1, wx.wxALL + wx.wxGROW + wx.wxCENTER+ wx.wxALIGN_CENTER_VERTICAL, 0)
findReplaceSizer:Add(16, 8, 0, wx.wxALL + wx.wxALIGN_RIGHT + wx.wxADJUST_MINSIZE,0)
if (infiles) then
findReplaceSizer:Add(infilesMaskStat, 0, wx.wxTOP + wx.wxALIGN_RIGHT + wx.wxALIGN_CENTER_VERTICAL, 5)
findReplaceSizer:Add(infilesMaskCombo, 1, wx.wxTOP + wx.wxGROW + wx.wxCENTER+ wx.wxALIGN_CENTER_VERTICAL, 5)
findReplaceSizer:Add(16, 8, 0, wx.wxTOP, 5)
findReplaceSizer:Add(infilesDirStat, 0, wx.wxTOP + wx.wxALIGN_RIGHT + wx.wxALIGN_CENTER_VERTICAL, 5)
findReplaceSizer:Add(infilesDirCombo, 1, wx.wxTOP + wx.wxGROW + wx.wxCENTER+ wx.wxALIGN_CENTER_VERTICAL, 5)
findReplaceSizer:Add(infilesDirButton, 0, wx.wxTOP + wx.wxALIGN_RIGHT + wx.wxADJUST_MINSIZE+ wx.wxALIGN_CENTER_VERTICAL, 5)
end
if (replace) then
findReplaceSizer:Add(replaceStatText, 0, wx.wxTOP + wx.wxALIGN_RIGHT + wx.wxALIGN_CENTER_VERTICAL, 5)
findReplaceSizer:Add(replaceTextCombo, 1, wx.wxTOP + wx.wxGROW + wx.wxCENTER+ wx.wxALIGN_CENTER_VERTICAL, 5)
end
-- the StaticBox(Sizer) needs to be created before checkboxes, otherwise
-- checkboxes don't get any clicks on OSX (ide.osname == 'Macintosh')
-- as the z-order for event traversal appears to be incorrect.
local optionsSizer = wx.wxStaticBoxSizer(wx.wxVERTICAL, findDialog, "Options" )
-- Create find/replace option checkboxes
local wholeWordCheckBox = wx.wxCheckBox(findDialog, wx.wxID_ANY, "Match &whole word")
local matchCaseCheckBox = wx.wxCheckBox(findDialog, wx.wxID_ANY, "Match &case")
local wrapAroundCheckBox = wx.wxCheckBox(findDialog, wx.wxID_ANY, "Wrap ar&ound")
local regexCheckBox = wx.wxCheckBox(findDialog, wx.wxID_ANY, "Regular &expression")
wholeWordCheckBox:SetValue(findReplace.fWholeWord)
matchCaseCheckBox:SetValue(findReplace.fMatchCase)
wrapAroundCheckBox:SetValue(findReplace.fWrap)
regexCheckBox:SetValue(findReplace.fRegularExpr)
local optionSizer = wx.wxBoxSizer(wx.wxVERTICAL, findDialog)
optionSizer:Add(wholeWordCheckBox, 0, wx.wxALL + wx.wxGROW + wx.wxCENTER, 3)
optionSizer:Add(matchCaseCheckBox, 0, wx.wxALL + wx.wxGROW + wx.wxCENTER, 3)
optionSizer:Add(wrapAroundCheckBox, 0, wx.wxALL + wx.wxGROW + wx.wxCENTER, 3)
optionSizer:Add(regexCheckBox, 0, wx.wxALL + wx.wxGROW + wx.wxCENTER, 3)
optionsSizer:Add(optionSizer, 0, 0, 5)
-- Create scope radiobox
local scopeRadioBox
local subDirCheckBox
local makeBakCheckBox
local scopeSizer
if (infiles) then
-- the StaticBox(Sizer) needs to be created before checkboxes
scopeSizer = wx.wxStaticBoxSizer(wx.wxVERTICAL, findDialog, "In Files")
subDirCheckBox = wx.wxCheckBox(findDialog, wx.wxID_ANY, "&Subdirectories")
makeBakCheckBox = wx.wxCheckBox(findDialog, wx.wxID_ANY, ".&bak on Replace")
subDirCheckBox:SetValue(findReplace.fSubDirs)
makeBakCheckBox:SetValue(findReplace.fMakeBak)
local optionSizer = wx.wxBoxSizer(wx.wxVERTICAL, findDialog)
optionSizer:Add(subDirCheckBox, 0, wx.wxALL + wx.wxGROW + wx.wxCENTER, 3)
optionSizer:Add(makeBakCheckBox, 0, wx.wxALL + wx.wxGROW + wx.wxCENTER, 3)
scopeSizer:Add(optionSizer, 0, 0, 5)
else
scopeRadioBox = wx.wxRadioBox(findDialog, wx.wxID_ANY, "Scope", wx.wxDefaultPosition, wx.wxDefaultSize, {"&Up", "&Down"}, 1, wx.wxRA_SPECIFY_COLS)
scopeRadioBox:SetSelection(iff(findReplace.fDown, 1, 0))
scopeSizer = wx.wxBoxSizer(wx.wxVERTICAL, findDialog)
scopeSizer:Add(scopeRadioBox, 0, 0, 0)
end
-- Add all the sizers to the dialog
local optionScopeSizer = wx.wxBoxSizer(wx.wxHORIZONTAL)
optionScopeSizer:Add(optionsSizer, 0, wx.wxALL + wx.wxGROW + wx.wxCENTER, 5)
optionScopeSizer:Add(scopeSizer, 0, wx.wxALL + wx.wxGROW + wx.wxCENTER, 5)
local leftSizer = wx.wxBoxSizer(wx.wxVERTICAL)
leftSizer:Add(findReplaceSizer, 0, wx.wxALL + wx.wxGROW + wx.wxCENTER, 0)
leftSizer:Add(optionScopeSizer, 0, wx.wxALL + wx.wxGROW + wx.wxCENTER, 0)
local mainSizer = wx.wxBoxSizer(wx.wxHORIZONTAL)
mainSizer:Add(leftSizer, 0, wx.wxALL + wx.wxGROW + wx.wxCENTER, 10)
mainSizer:Add(buttonsSizer, 0, wx.wxALL + wx.wxGROW + wx.wxCENTER, 10)
mainSizer:SetSizeHints( findDialog )
findDialog:SetSizer(mainSizer)
local function TransferDataFromWindow()
findReplace.fWholeWord = wholeWordCheckBox:GetValue()
findReplace.fMatchCase = matchCaseCheckBox:GetValue()
findReplace.fWrap = wrapAroundCheckBox:GetValue()
if (findReplace.infiles) then
findReplace.fSubDirs = subDirCheckBox:GetValue()
findReplace.fMakeBak = makeBakCheckBox:GetValue()
else
findReplace.fDown = scopeRadioBox:GetSelection() == 1
end
findReplace.fRegularExpr = regexCheckBox:GetValue()
findReplace.findText = findTextCombo:GetValue()
PrependStringToArray(findReplace.findTextArray, findReplace.findText)
if findReplace.replace then
findReplace.replaceText = replaceTextCombo:GetValue()
PrependStringToArray(findReplace.replaceTextArray, findReplace.replaceText)
end
if findReplace.infiles then
findReplace.filemaskText = infilesMaskCombo:GetValue()
PrependStringToArray(findReplace.filemaskTextArray, findReplace.filemaskText)
findReplace.filedirText = infilesDirCombo:GetValue()
PrependStringToArray(findReplace.filedirTextArray, findReplace.filedirText)
end
return true
end
findDialog:Connect(ID_FIND_NEXT, wx.wxEVT_COMMAND_BUTTON_CLICKED,
function()
TransferDataFromWindow()
if (findReplace.infiles) then
findReplace:RunInFiles()
findReplace.dialog:Destroy()
else
findReplace:FindString()
end
end)
findDialog:Connect(ID_REPLACE, wx.wxEVT_COMMAND_BUTTON_CLICKED,
function(event)
TransferDataFromWindow()
event:Skip()
if findReplace.replace then
if (findReplace.infiles) then
findReplace:RunInFiles(true)
findReplace.dialog:Destroy()
else
findReplace:ReplaceString()
end
else
findReplace.dialog:Destroy()
findReplace.dialog = createFindReplaceDialog(true,infiles)
findReplace.dialog:Show(true)
end
end)
if replaceAllButton then
findDialog:Connect(ID_REPLACE_ALL, wx.wxEVT_COMMAND_BUTTON_CLICKED,
function(event)
TransferDataFromWindow()
event:Skip()
findReplace:ReplaceString(true)
end)
end
if infilesDirButton then
findDialog:Connect(ID_SETDIR, wx.wxEVT_COMMAND_BUTTON_CLICKED,
function()
local filePicker = wx.wxDirDialog(findDialog, "Choose a project directory",
findReplace.filedirText~="" and findReplace.filedirText or wx.wxGetCwd(),wx.wxFLP_USE_TEXTCTRL)
local res = filePicker:ShowModal(true)
if res == wx.wxID_OK then
infilesDirCombo:SetValue(filePicker:GetPath())
end
end)
end
findDialog:Connect(wx.wxID_ANY, wx.wxEVT_CLOSE_WINDOW,
function (event)
TransferDataFromWindow()
event:Skip()
findDialog:Show(false)
findDialog:Destroy()
end)
return findDialog
end
function findReplace:Show(replace,infiles)
self.dialog = nil
self.dialog = createFindReplaceDialog(replace,infiles)
self.dialog:Show(true)
end