Files
OpenRA/src/editor/menu_search.lua
2015-01-07 15:41:49 -08:00

398 lines
15 KiB
Lua

-- Copyright 2011-14 Paul Kulchenko, ZeroBrane LLC
-- authors: Lomtik Software (J. Winwood & John Labenski)
-- Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
local ide = ide
local q = EscapeMagic
local unpack = table.unpack or unpack
local frame = ide.frame
local menuBar = frame.menuBar
local findReplace = ide.findReplace
local findMenu = wx.wxMenu{
{ ID_FIND, TR("&Find")..KSC(ID_FIND), TR("Find text") },
{ ID_FINDNEXT, TR("Find &Next")..KSC(ID_FINDNEXT), TR("Find the next text occurrence") },
{ ID_FINDPREV, TR("Find &Previous")..KSC(ID_FINDPREV), TR("Find the earlier text occurence") },
{ ID_FINDSELECTNEXT, TR("Select and Find Next")..KSC(ID_FINDSELECTNEXT), TR("Select the word under cursor and find its next occurrence") },
{ ID_FINDSELECTPREV, TR("Select and Find Previous")..KSC(ID_FINDSELECTPREV), TR("Select the word under cursor and find its previous occurrence") },
{ ID_REPLACE, TR("&Replace")..KSC(ID_REPLACE), TR("Find and replace text") },
{ },
{ ID_FINDINFILES, TR("Find &In Files")..KSC(ID_FINDINFILES), TR("Find text in files") },
{ ID_REPLACEINFILES, TR("Re&place In Files")..KSC(ID_REPLACEINFILES), TR("Find and replace text in files") },
{ },
}
findMenu:Append(ID_NAVIGATE, TR("Navigate"), wx.wxMenu {
{ ID_NAVIGATETOFILE, TR("Go To File...")..KSC(ID_NAVIGATETOFILE), TR("Go to file") },
{ ID_NAVIGATETOLINE, TR("Go To Line...")..KSC(ID_NAVIGATETOLINE), TR("Go to line") },
{ ID_NAVIGATETOSYMBOL, TR("Go To Symbol...")..KSC(ID_NAVIGATETOSYMBOL), TR("Go to symbol") },
{ ID_NAVIGATETOMETHOD, TR("Insert Library Function...")..KSC(ID_NAVIGATETOMETHOD), TR("Find and insert library function") },
})
menuBar:Append(findMenu, TR("&Search"))
local function onUpdateUISearchMenu(event) event:Enable(GetEditor() ~= nil) end
frame:Connect(ID_FIND, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
findReplace:Show(false)
end)
frame:Connect(ID_FIND, wx.wxEVT_UPDATE_UI, onUpdateUISearchMenu)
frame:Connect(ID_REPLACE, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
findReplace:Show(true)
end)
frame:Connect(ID_REPLACE, wx.wxEVT_UPDATE_UI, onUpdateUISearchMenu)
frame:Connect(ID_FINDINFILES, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
findReplace:Show(false,true)
end)
frame:Connect(ID_REPLACEINFILES, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
findReplace:Show(true,true)
end)
frame:Connect(ID_FINDNEXT, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
local editor = GetEditor()
if editor and ide.wxver >= "2.9.5" and editor:GetSelections() > 1 then
local selection = editor:GetMainSelection() + 1
if selection >= editor:GetSelections() then selection = 0 end
editor:SetMainSelection(selection)
editor:ShowPosEnforcePolicy(editor:GetCurrentPos())
else
if findReplace:GetSelectedString() or findReplace:HasText() then
findReplace:FindString()
else
findReplace:Show(false)
end
end
end)
frame:Connect(ID_FINDNEXT, wx.wxEVT_UPDATE_UI, onUpdateUISearchMenu)
frame:Connect(ID_FINDPREV, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
local editor = GetEditor()
if editor and ide.wxver >= "2.9.5" and editor:GetSelections() > 1 then
local selection = editor:GetMainSelection() - 1
if selection < 0 then selection = editor:GetSelections() - 1 end
editor:SetMainSelection(selection)
editor:ShowPosEnforcePolicy(editor:GetCurrentPos())
else
if findReplace:GetSelectedString() or findReplace:HasText() then
findReplace:FindString(true) -- search up
else
findReplace:Show(false)
end
end
end)
frame:Connect(ID_FINDPREV, wx.wxEVT_UPDATE_UI, onUpdateUISearchMenu)
-- Select and Find behaves like Find if there is a current selection;
-- if not, it selects a word under cursor (if any) and does find.
local function selectWordUnderCaret(editor)
local pos = editor:GetCurrentPos()
local text = editor:GetTextRange( -- try to select a word under caret
editor:WordStartPosition(pos, true), editor:WordEndPosition(pos, true))
return #text > 0 and text or editor:GetTextRange( -- try to select a non-word under caret
editor:WordStartPosition(pos, false), editor:WordEndPosition(pos, false))
end
frame:Connect(ID_FINDSELECTNEXT, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
local editor = GetEditor()
if editor:GetSelectionStart() ~= editor:GetSelectionEnd() then
ide.frame:AddPendingEvent(
wx.wxCommandEvent(wx.wxEVT_COMMAND_MENU_SELECTED, ID_FINDNEXT))
return
end
local text = selectWordUnderCaret(editor)
if #text > 0 then
findReplace.findText = text
findReplace:FindString()
end
end)
frame:Connect(ID_FINDSELECTNEXT, wx.wxEVT_UPDATE_UI, onUpdateUISearchMenu)
frame:Connect(ID_FINDSELECTPREV, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
local editor = GetEditor()
if editor:GetSelectionStart() ~= editor:GetSelectionEnd() then
ide.frame:AddPendingEvent(
wx.wxCommandEvent(wx.wxEVT_COMMAND_MENU_SELECTED, ID_FINDPREV))
return
end
local text = selectWordUnderCaret(editor)
if #text > 0 then
findReplace.findText = text
findReplace:FindString(true)
end
end)
frame:Connect(ID_FINDSELECTPREV, wx.wxEVT_UPDATE_UI, onUpdateUISearchMenu)
local markername = "commandbar.background"
local mac = ide.osname == 'Macintosh'
local win = ide.osname == 'Windows'
local special = {SYMBOL = '@', LINE = ':', METHOD = ';'}
local function navigateTo(default, selected)
local styles = ide.config.styles
local marker = ide:AddMarker(markername,
wxstc.wxSTC_MARK_BACKGROUND, styles.text.fg, styles.caretlinebg.bg)
local nb = ide:GetEditorNotebook()
local selection = nb:GetSelection()
local files, preview, origline, functions, methods
local function markLine(ed, toline)
ed:MarkerDefine(ide:GetMarker(markername))
ed:MarkerDeleteAll(marker)
ed:MarkerAdd(toline-1, marker)
-- store the original line if not stored yet
origline = origline or (ed:GetCurrentLine()+1)
ed:EnsureVisibleEnforcePolicy(toline-1)
end
CommandBarShow({
defaultText = default or "",
selectedText = selected or "",
onDone = function(t, enter, text)
if not mac then nb:Freeze() end
-- delete all current line markers if any; restore line position
local ed = ide:GetEditor()
if ed and origline then
ed:MarkerDeleteAll(marker)
ed:EnsureVisibleEnforcePolicy(origline-1)
end
if enter then
local fline, sline, tabindex = unpack(t or {})
local ed = ide:GetEditor()
-- jump to symbol; tabindex has the position of the symbol
if text and text:find(special.SYMBOL) and tabindex then
ed:GotoPos(tabindex-1)
ed:EnsureVisibleEnforcePolicy(ed:LineFromPosition(tabindex-1))
ed:SetFocus() -- in case the focus is on some other panel
-- insert selected method
elseif text and text:find('^%s*'..special.METHOD) then
if ed then -- clean up text and insert at the current location
local method = sline
local isfunc = methods.desc[method][1]:find(q(method).."%s*%(")
local text = method .. (isfunc and "()" or "")
local pos = ed:GetCurrentPos()
ed:InsertText(pos, text)
ed:EnsureVisibleEnforcePolicy(ed:LineFromPosition(pos))
ed:GotoPos(pos + #method + (isfunc and 1 or 0))
if isfunc then -- show the tooltip
ide.frame:AddPendingEvent(wx.wxCommandEvent(
wx.wxEVT_COMMAND_MENU_SELECTED, ID_SHOWTOOLTIP))
end
end
-- set line position in the (current) editor if requested
elseif text and text:find(special.LINE) then
local toline = tonumber(text:match(special.LINE..'(%d+)'))
if toline and ed then
ed:GotoLine(toline-1)
ed:EnsureVisibleEnforcePolicy(toline-1)
ed:SetFocus() -- in case the focus is on some other panel
end
elseif tabindex then -- switch to existing tab
SetEditorSelection(tabindex)
if preview then -- close preview if not selected
local pindex = nb:GetPageIndex(preview)
if pindex ~= tabindex then ClosePage(pindex) end
end
elseif sline then -- load a new file (into preview if set)
LoadFile(MergeFullPath(ide:GetProject(), sline), preview or nil, true)
end
else
-- close preview
if preview then ClosePage(nb:GetPageIndex(preview)) end
-- restore original selection if canceled
if nb:GetSelection() ~= selection then nb:SetSelection(selection) end
end
preview = nil
if not mac then nb:Thaw() end
end,
onUpdate = function(text)
local lines = {}
local projdir = ide:GetProject()
-- delete all current line markers if any
-- restore the original position if search text is updated
local ed = ide:GetEditor()
if ed and origline then ed:MarkerDeleteAll(marker) end
-- reset cached functions if no symbol search
if text and not text:find(special.SYMBOL) then
functions = nil
if ed and origline then ed:EnsureVisibleEnforcePolicy(origline-1) end
end
-- reset cached methods if no method search
if text and not text:find(special.METHOD) then methods = nil end
if ed and text and text:find(special.SYMBOL) then
if not functions then
local funcs, nums = OutlineFunctions(ed), {}
functions = {pos = {}, src = {}}
for _, func in ipairs(funcs) do
table.insert(functions, func.name)
nums[func.name] = (nums[func.name] or 0) + 1
local num = nums[func.name]
local line = ed:LineFromPosition(func.pos-1)
functions.src[func.name..num] = ed:GetLine(line):gsub("^%s+","")
functions.pos[func.name..num] = func.pos
end
end
local symbol = text:match(special.SYMBOL..'(.*)')
local nums = {}
if #symbol > 0 then
local topscore
for _, item in ipairs(CommandBarScoreItems(functions, symbol, 100)) do
local func, score = unpack(item)
topscore = topscore or score
nums[func] = (nums[func] or 0) + 1
local num = nums[func]
if score > topscore / 4 and score > 1 then
table.insert(lines, {("%2d %s"):format(score, func),
functions.src[func..num], functions.pos[func..num]})
end
end
else
for n, name in ipairs(functions) do
nums[name] = (nums[name] or 0) + 1
local num = nums[name]
lines[n] = {name, functions.src[name..num], functions.pos[name..num]}
end
end
elseif ed and text and text:find('^%s*'..special.METHOD) then
if not methods then
methods = {desc = {}}
local num = 1
if ed.api and ed.api.tip and ed.api.tip.shortfinfoclass then
for libname, lib in pairs(ed.api.tip.shortfinfoclass) do
for method, val in pairs(lib) do
local signature, desc = val:match('(.-)\n(.*)')
local m = libname..'.'..method
desc = desc and desc:gsub("\n", " ") or val
methods[num] = m
methods.desc[m] = {signature or (libname..'.'..method), desc}
num = num + 1
end
end
end
end
local method = text:match(special.METHOD..'(.*)')
if #method > 0 then
local topscore
for _, item in ipairs(CommandBarScoreItems(methods, method, 100)) do
local method, score = unpack(item)
topscore = topscore or score
if score > topscore / 4 and score > 1 then
table.insert(lines, { score, method })
end
end
end
elseif text and text:find(special.LINE) then
local toline = tonumber(text:match(special.LINE..'(%d+)'))
if toline and ed then markLine(ed, toline) end
elseif text and #text > 0 and projdir and #projdir > 0 then
-- populate the list of files
if not files then
files = FileSysGetRecursive(projdir, true)
for k = #files, 1, -1 do
if IsDirectory(files[k]) then
table.remove(files, k)
else
files[k] = files[k]:gsub("^"..q(projdir), "")
end
end
end
local topscore
for _, item in ipairs(CommandBarScoreItems(files, text, 100)) do
local file, score = unpack(item)
topscore = topscore or score
if score > topscore / 4 and score > 1 then
table.insert(lines, {
("%2d %s"):format(score, wx.wxFileName(file):GetFullName()),
file,
})
end
end
else
for _, doc in pairs(ide:GetDocuments()) do
lines[doc:GetTabIndex()+1] = {doc:GetFileName(), doc:GetFilePath(), doc:GetTabIndex()}
end
end
return lines
end,
onItem = function(t)
if methods then
local score, method = unpack(t)
return ("%2d %s"):format(score, methods.desc[method][1]), methods.desc[method][2]
else
return unpack(t)
end
end,
onSelection = function(t, text)
local _, file, tabindex = unpack(t)
if text and text:find(special.SYMBOL) then
local ed = ide:GetEditor()
if ed then markLine(ed, ed:LineFromPosition(tabindex-1)+1) end
return
elseif text and text:find(special.METHOD) then
return
end
if file then file = MergeFullPath(ide:GetProject(), file) end
-- disabling event handlers for the notebook and the editor
-- to minimize changes in the UI when editors are switched
-- or files in the preview are updated.
nb:SetEvtHandlerEnabled(false)
local doc = file and ide:FindDocument(file)
if doc and not tabindex then tabindex = doc:GetTabIndex() end
if tabindex then
local ed = nb:GetPage(tabindex)
ed:SetEvtHandlerEnabled(false)
if nb:GetSelection() ~= tabindex then nb:SetSelection(tabindex) end
ed:SetEvtHandlerEnabled(true)
elseif file then
-- skip binary files with unknown extensions
if #ide:GetKnownExtensions(GetFileExt(file)) > 0
or not isBinary(FileRead(file, 2048)) then
preview = preview or NewFile()
preview:SetEvtHandlerEnabled(false)
LoadFile(file, preview, true, true)
preview:SetFocus()
-- force refresh since the panel covers the editor on OSX/Linux
-- this fixes the preview window not always redrawn on Linux
if not win then preview:Update() preview:Refresh() end
preview:SetEvtHandlerEnabled(true)
elseif preview then
ClosePage(nb:GetPageIndex(preview))
preview = nil
end
end
nb:SetEvtHandlerEnabled(true)
end,
})
end
frame:Connect(ID_NAVIGATETOFILE, wx.wxEVT_COMMAND_MENU_SELECTED,
function() navigateTo("") end)
frame:Connect(ID_NAVIGATETOLINE, wx.wxEVT_COMMAND_MENU_SELECTED,
function() navigateTo(special.LINE) end)
frame:Connect(ID_NAVIGATETOMETHOD, wx.wxEVT_COMMAND_MENU_SELECTED,
function() navigateTo(special.METHOD) end)
frame:Connect(ID_NAVIGATETOSYMBOL, wx.wxEVT_COMMAND_MENU_SELECTED,
function()
local ed = GetEditor()
navigateTo(special.SYMBOL, ed and ed:ValueFromPosition(ed:GetCurrentPos()))
end)